AWS CloudFormation: variables personalizadas en plantillas

18

¿Hay alguna forma de definir accesos directos para los valores de uso frecuente derivados de los parámetros de la plantilla CloudFormation?

Por ejemplo: tengo un script que crea una pila de proyectos Multi-AZ con el nombre ELB projecty dos instancias detrás del ELB llamado project-1y project-2. Solo paso el ELBHostNameparámetro a la plantilla y luego lo uso para construir:

"Fn::Join": [
    ".", [
        { "Fn::Join": [ "", [ { "Ref": "ELBHostName" }, "-1" ] ] },
        { "Ref": "EnvironmentVersioned" },
        { "Ref": "HostedZone" }
    ]
]

Esta construcción o muy similar se repite muchas veces a lo largo de la plantilla, para crear el nombre de host EC2, los registros de Route53, etc.

En lugar de repetir eso una y otra vez, me gustaría asignar el resultado de eso Fn::Joina una variable de algún tipo y solo referirme a eso, al igual que puedo con la "Ref":declaración.

Idealmente algo como:

Var::HostNameFull = "Fn::Join": [ ... ]
...
{ "Name": { "Ref": "Var::HostNameFull" } }

o algo similarmente simple.

¿Es eso posible con Amazon CloudFormation?

MLu
fuente
¿ELBHostName completo es un parámetro que está pasando explícitamente a Cloudformation? Si es así, ¿por qué usar una referencia? Podría usar Moustache para incluir variables en su plantilla y transformar eso a JSON antes de enviarlo a Cloudformation. Depende de cómo se ve el proceso de aprovisionamiento.
Canuteson

Respuestas:

5

Estaba buscando la misma funcionalidad. El uso de una pila anidada como SpoonMeiser sugirió vino a mi mente, pero luego me di cuenta de que lo que realmente necesitaba eran funciones personalizadas. Afortunadamente, CloudFormation permite el uso de AWS :: CloudFormation :: CustomResource que, con un poco de trabajo, le permite a uno hacer exactamente eso. Esto se siente como una exageración solo para variables (algo que diría que debería haber estado en CloudFormation en primer lugar), pero hace el trabajo y, además, permite toda la flexibilidad de (elija Python / node /Java). Cabe señalar que las funciones lambda cuestan dinero, pero aquí estamos hablando de centavos a menos que cree / elimine sus pilas varias veces por hora.

El primer paso es hacer que una función lambda en esta página no haga nada más que tomar el valor de entrada y copiarlo a la salida. Podríamos hacer que la función lambda haga todo tipo de locuras, pero una vez que tenemos la función de identidad, cualquier otra cosa es fácil. Alternativamente, podríamos crear la función lambda en la pila misma. Como uso muchas pilas en 1 cuenta, tendría un montón de funciones y roles lambda sobrantes (y todas las pilas deben crearse --capabilities=CAPABILITY_IAM, ya que también necesita un rol.

Crear función lambda

  • Vaya a la página de inicio de lambda y seleccione su región favorita
  • Seleccione "Función en blanco" como plantilla
  • Haga clic en "Siguiente" (no configure ningún desencadenante)
  • Llenar:
    • Nombre: CloudFormationIdentity
    • Descripción: Devuelve lo que se obtiene, soporte variable en la formación en la nube
    • Tiempo de ejecución: python2.7
    • Tipo de entrada de código: Editar código en línea
    • Código: ver abajo
    • Manipulador: index.handler
    • Rol: crear un rol personalizado. En este punto, se abre una ventana emergente que le permite crear una nueva función. Acepte todo en esta página y haga clic en "Permitir". Creará un rol con permisos para publicar en los registros de cloudwatch.
    • Memoria: 128 (este es el mínimo)
    • Tiempo de espera: 3 segundos (debería ser suficiente)
    • VPC: sin VPC

Luego copie y pegue el código a continuación en el campo de código. La parte superior de la función es el código del módulo de python cfn-response , que solo se instala automáticamente si la función lambda se crea a través de CloudFormation, por alguna extraña razón. La handlerfunción es bastante autoexplicativa.

from __future__ import print_function
import json

try:
    from urllib2 import HTTPError, build_opener, HTTPHandler, Request
except ImportError:
    from urllib.error import HTTPError
    from urllib.request import build_opener, HTTPHandler, Request


SUCCESS = "SUCCESS"
FAILED = "FAILED"


def send(event, context, response_status, reason=None, response_data=None, physical_resource_id=None):
    response_data = response_data or {}
    response_body = json.dumps(
        {
            'Status': response_status,
            'Reason': reason or "See the details in CloudWatch Log Stream: " + context.log_stream_name,
            'PhysicalResourceId': physical_resource_id or context.log_stream_name,
            'StackId': event['StackId'],
            'RequestId': event['RequestId'],
            'LogicalResourceId': event['LogicalResourceId'],
            'Data': response_data
        }
    )
    if event["ResponseURL"] == "http://pre-signed-S3-url-for-response":
        print("Would send back the following values to Cloud Formation:")
        print(response_data)
        return

    opener = build_opener(HTTPHandler)
    request = Request(event['ResponseURL'], data=response_body)
    request.add_header('Content-Type', '')
    request.add_header('Content-Length', len(response_body))
    request.get_method = lambda: 'PUT'
    try:
        response = opener.open(request)
        print("Status code: {}".format(response.getcode()))
        print("Status message: {}".format(response.msg))
        return True
    except HTTPError as exc:
        print("Failed executing HTTP request: {}".format(exc.code))
        return False

def handler(event, context):
    responseData = event['ResourceProperties']
    send(event, context, SUCCESS, None, responseData, "CustomResourcePhysicalID")
  • Haga clic en Siguiente"
  • Haga clic en "Crear función"

Ahora puede probar la función lambda seleccionando el botón "Prueba" y seleccione "Solicitud de creación de CloudFormation" como plantilla de muestra. Debería ver en su registro que se devuelven las variables alimentadas.

Usar variable en su plantilla de CloudFormation

Ahora que tenemos esta función lambda, podemos usarla en las plantillas de CloudFormation. Primero tome nota de la función lambda Arn (vaya a la página de inicio lambda , haga clic en la función recién creada, el Arn debe estar en la parte superior derecha, algo así como arn:aws:lambda:region:12345:function:CloudFormationIdentity).

Ahora en su plantilla, en la sección de recursos, especifique sus variables como:

Identity:
  Type: "Custom::Variable"
  Properties:
    ServiceToken: "arn:aws:lambda:region:12345:function:CloudFormationIdentity"
    Arn: "arn:aws:lambda:region:12345:function:CloudFormationIdentity"

ClientBucketVar:
  Type: "Custom::Variable"
  Properties:
    ServiceToken: !GetAtt [Identity, Arn]
    Name: !Join ["-", [my-client-bucket, !Ref ClientName]]
    Arn: !Join [":", [arn, aws, s3, "", "", !Join ["-", [my-client-bucket, !Ref ClientName]]]]

ClientBackupBucketVar:
  Type: "Custom::Variable"
  Properties:
    ServiceToken: !GetAtt [Identity, Arn]
    Name: !Join ["-", [my-client-bucket, !Ref ClientName, backup]]
    Arn: !Join [":", [arn, aws, s3, "", "", !Join ["-", [my-client-bucket, !Ref ClientName, backup]]]]

Primero especifico una Identityvariable que contiene el Arn para la función lambda. Poner esto en una variable aquí, significa que solo tengo que especificarlo una vez. Hago todas mis variables de tipo Custom::Variable. CloudFormation le permite usar cualquier tipo de nombre que comience con Custom::recursos personalizados.

Tenga en cuenta que la Identityvariable contiene el Arn para la función lambda dos veces. Una vez para especificar la función lambda a utilizar. La segunda vez como el valor de la variable.

Ahora que tengo la Identityvariable, puedo definir nuevas variables usando ServiceToken: !GetAtt [Identity, Arn](creo que el código JSON debería ser algo así como "ServiceToken": {"Fn::GetAtt": ["Identity", "Arn"]}). Creo 2 nuevas variables, cada una con 2 campos: Nombre y Arn. En el resto de mi plantilla puedo usar !GetAtt [ClientBucketVar, Name]o !GetAtt [ClientBucketVar, Arn]cuando lo necesite.

Palabra de precaución

Al trabajar con recursos personalizados, si la función lambda se bloquea, está atascado entre 1 y 2 horas, porque CloudFormation espera una respuesta de la función (bloqueada) durante una hora antes de darse por vencido. Por lo tanto, podría ser bueno especificar un tiempo de espera corto para la pila mientras se desarrolla la función lambda.

Claude
fuente
Respuesta impresionante! Lo leí y lo ejecuté en mis pilas, aunque para mí, no me preocupa la proliferación de funciones lambda en mi cuenta y me gustan las plantillas independientes (modularizo usando la cloudformation-toolgema), así que empaco la creación lambda en la plantilla y luego puede usarla directamente en lugar de crear el Identityrecurso personalizado. Vea aquí mi código: gist.github.com/guss77/2471e8789a644cac96992c4102936fb3
Guss
Cuando estás "... estás atrapado entre 1 y 2 horas ..." porque una lambda se estrelló y no respondió con una respuesta cfn, puedes hacer que la plantilla se mueva nuevamente usando manualmente curl / wget en la URL firmada Solo asegúrese de imprimir siempre el evento / URL al comienzo de la lambda para que pueda ir a CloudWatch y obtener la URL si se bloquea.
Taylor
12

No tengo una respuesta, pero quería señalar que puede ahorrarse mucho dolor al usarlo Fn::Suben lugar deFn::Join

{ "Fn::Sub": "${ELBHostName"}-1.${EnvironmentVersioned}.${HostedZone}"}

Reemplaza

"Fn::Join": [
    ".", [
        { "Fn::Join": [ "", [ { "Ref": "ELBHostName" }, "-1" ] ] },
        { "Ref": "EnvironmentVersioned" },
        { "Ref": "HostedZone" }
    ]
]
Kevin Audleman
fuente
3

No. Lo intenté, pero me quedé vacío. La forma que tenía sentido para mí era crear una entrada de asignaciones llamada "CustomVariables" y tener esa casa todas mis variables. Funciona para cadenas simples, pero no puede usar Intrinsics (Refs, Fn :: Joins, etc.) dentro de Mappings .

Trabajos:

"Mappings" : {
  "CustomVariables" : {
    "Variable1" : { "Value" : "foo" },
    "Variable2" : { "Value" : "bar" }
  }
}

No funcionará

  "Variable3" : { "Value" : { "Ref" : "AWS::Region" } }

Eso es solo un ejemplo. No pondrías una referencia independiente en una variable.

Robar
fuente
1
La documentación dice que los valores de mapeo deben ser cadenas literales.
Ivan Anishchuk el
3

Podría usar una pila anidada que resuelva todas sus variables en sus salidas, y luego usarla Fn::GetAttpara leer las salidas de esa pila

SpoonMeiser
fuente
2

Puede usar plantillas anidadas en las que "resuelve" todas sus variables en la plantilla externa y las pasa a otra plantilla.

JoseOlcese
fuente