AWS CloudFormation - Variáveis personalizadas em modelos

15

Existe alguma maneira de definir atalhos para valores usados com frequência derivados de parâmetros de modelo do CloudFormation?

Por exemplo - Eu tenho um script que cria uma pilha de Projetos Multi-AZ com o nome ELB project e duas instâncias por trás do ELB chamadas project-1 e project-2 . Eu só passo o parâmetro ELBHostName para o template e depois uso ele para construir:

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

Essa construção ou muito semelhante é repetida várias vezes em todo o modelo - para criar o nome do host do EC2, os registros do Route53, etc.

Em vez de repetir isso repetidas vezes, gostaria de atribuir a saída desse Fn::Join a uma variável de algum tipo e referir-me apenas a isso, assim como posso com a instrução "Ref": .

Idealmente, algo como:

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

ou algo similarmente simples.

Isso é possível com o Amazon CloudFormation?

    
por MLu 02.09.2014 / 05:41

5 respostas

4

Eu estava procurando a mesma funcionalidade. Usando uma pilha aninhada como SpoonMeiser sugerido veio à mente, mas depois percebi que o que eu realmente precisava era de funções personalizadas. Felizmente, o CloudFormation permite o uso de AWS :: CloudFormation :: CustomResource que, com um pouco de trabalho, permite que se faça exatamente isso. Isso parece um exagero para apenas variáveis (algo que eu diria que deveria ter sido no CloudFormation, em primeiro lugar), mas ele faz o trabalho e, além disso, permite toda a flexibilidade de (escolha a opção python / node /Java). Deve-se notar que as funções lambda custam dinheiro, mas estamos falando de tostões aqui, a menos que você crie / apague suas pilhas várias vezes por hora.

O primeiro passo é fazer uma função lambda nesta página que não faz nada além de pegue o valor de entrada e copie-o para a saída. Poderíamos ter a função lambda fazendo todo tipo de coisa maluca, mas uma vez que tenhamos a função de identidade, qualquer outra coisa é fácil. Alternativamente, poderíamos ter a função lambda sendo criada na própria pilha. Como eu uso muitas pilhas em uma conta, eu teria um monte de funções e funções lambda restantes (e todas as pilhas precisam ser criadas com --capabilities=CAPABILITY_IAM , pois também precisa de um papel.

Criar função lambda

  • Acesse a página inicial do lambda e selecione o seu região favorita
  • Selecione "Função em branco" como modelo
  • Clique em "Próximo" (não configure nenhum acionador)
  • Preencha:
    • Nome: CloudFormationIdentity
    • Descrição: Retorna o que é obtido, suporte a variáveis na Cloud Formation
    • Tempo de execução: python2.7
    • Tipo de entrada de código: Editar código in-line
    • Código: veja abaixo
    • Manipulador: index.handler
    • Função: crie uma função personalizada. Nesse ponto, um pop-up é aberto, permitindo que você crie uma nova função. Aceite tudo nesta página e clique em "Permitir". Ele criará uma função com permissões para postar nos logs do cloudwatch.
    • Memória: 128 (este é o mínimo)
    • Tempo limite: 3 segundos (deve ser suficiente)
    • VPC: sem VPC

Copie e cole o código abaixo no campo de código. A parte superior da função é o código do módulo python de resposta , que só recebe auto-instalado se a função lambda é criada através do CloudFormation, por algum motivo estranho. A função handler é 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")
  • Clique em "Próximo"
  • Clique em "Criar função"

Agora você pode testar a função lambda selecionando o botão "Test" e selecionando "CloudFormation Create Request" como modelo de amostra. Você deve ver em seu log que as variáveis alimentadas são retornadas.

Use variáveis no seu modelo do CloudFormation

Agora que temos essa função lambda, podemos usá-la nos modelos do CloudFormation. Primeiro tome nota da função lambda Arn (vá para a página inicial do lambda , clique no botão apenas a função criada, o Arn deve estar no canto superior direito, algo como arn:aws:lambda:region:12345:function:CloudFormationIdentity ).

Agora, no seu modelo, na seção de recursos, especifique suas variáveis 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]]]]

Primeiramente eu especifico uma variável Identity que contém o Arn para a função lambda. Colocar isso em uma variável aqui significa que eu só preciso especificá-lo uma vez. Eu faço todas as minhas variáveis do tipo Custom::Variable . O CloudFormation permite que você use qualquer nome de tipo que comece com Custom:: para recursos personalizados.

Observe que a variável Identity contém o Arn para a função lambda duas vezes. Uma vez para especificar a função lambda a ser usada. A segunda vez como o valor da variável.

Agora que tenho a variável Identity , posso definir novas variáveis usando ServiceToken: !GetAtt [Identity, Arn] (acho que o código JSON deve ser algo como "ServiceToken": {"Fn::GetAtt": ["Identity", "Arn"]} ). Eu crio 2 novas variáveis, cada uma com 2 campos: Name e Arn. No restante do meu modelo, posso usar !GetAtt [ClientBucketVar, Name] ou !GetAtt [ClientBucketVar, Arn] sempre que precisar.

Palavra de cautela

Ao trabalhar com recursos personalizados, se a função lambda travar, você fica preso entre 1 e 2 horas, porque o CloudFormation aguarda uma resposta da função (travada) por uma hora antes de desistir. Portanto, pode ser bom especificar um tempo limite curto para a pilha enquanto desenvolve sua função lambda.

    
por 29.10.2016 / 15:17
10

Não tenho uma resposta, mas queria salientar que você pode poupar muita dor usando Fn::Sub no lugar de Fn::Join

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

Substitui

"Fn::Join": [
    ".", [
        { "Fn::Join": [ "", [ { "Ref": "ELBHostName" }, "-1" ] ] },
        { "Ref": "EnvironmentVersioned" },
        { "Ref": "HostedZone" }
    ]
]
    
por 18.10.2017 / 19:59
3

Não. Eu tentei, mas cheguei vazio. A maneira que fazia sentido para mim era criar uma entrada de mapeamentos chamada "CustomVariables" e fazer com que aquela casa todas as minhas variáveis. Funciona para Strings simples, mas você não pode usar Intrinsics (Refs, Fn: : Junções, etc.) dentro de Mapeamentos .

Trabalhos:

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

não funciona:

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

Isso é apenas um exemplo. Você não colocaria um Ref autônomo em uma variável.

    
por 05.09.2014 / 19:17
2

Você pode usar modelos aninhados nos quais "resolve" todas as suas variáveis no modelo externo e passá-las para outro modelo.

    
por 31.10.2014 / 22:38
2

Você poderia usar uma pilha aninhada que resolve todas as suas variáveis em suas saídas, e então usar Fn::GetAtt para ler as saídas daquela pilha

    
por 30.08.2016 / 12:55