AWS CloudFormation - Variáveis ​​personalizadas em modelos

18

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

Por exemplo - eu tenho um script que cria uma pilha do Projeto Multi-AZ com o nome ELB projecte duas instâncias atrás do ELB chamado project-1e project-2. Eu só passo o ELBHostNameparâmetro para o modelo e depois o uso 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 EC2, os registros do Route53, etc.

Em vez de repetir isso repetidamente, gostaria de atribuir a saída disso Fn::Joina uma variável de algum tipo e me referir apenas a isso, assim como posso com a "Ref":declaração.

Idealmente, algo como:

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

ou algo igualmente simples.

Isso é possível com o Amazon CloudFormation?

MLu
fonte
ELBHostName completo é um parâmetro que você está passando explicitamente para o Cloudformation? Se sim, por que usar um Ref? Pode usar o Bigode para incluir variáveis ​​em seu modelo e transformá-lo em JSON antes de enviá-lo para o Cloudformation. Depende da aparência do processo de provisionamento.
Canuteson

Respostas:

5

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

O primeiro passo é criar uma função lambda nesta página que não faça nada além de pegar o valor de entrada e copiá-lo para a saída. Poderíamos fazer com que a função lambda fizesse todo tipo de coisa maluca, mas uma vez que tenhamos a função de identidade, qualquer outra coisa será fácil. Como alternativa, poderíamos ter a função lambda sendo criada na própria pilha. Como eu uso muitas pilhas em uma conta, eu teria várias funções e funções restantes do lambda (e todas as pilhas precisam ser criadas com --capabilities=CAPABILITY_IAM, pois ela também precisa de uma função).

Criar função lambda

  • Vá para a página inicial lambda e selecione sua região favorita
  • Selecione "Função em branco" como modelo
  • Clique em "Avançar" (não configure nenhum gatilho)
  • Preencha:
    • Nome: CloudFormationIdentity
    • Descrição: retorna o que recebe, suporte variável na formação de nuvens
    • Tempo de execução: python2.7
    • Tipo de entrada de código: Editar código embutido
    • Código: veja abaixo
    • Manipulador: index.handler
    • Função: crie uma função personalizada. Nesse momento, 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

Em seguida, 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 cfn-response , que só é instalado automaticamente se a função lambda for criada por meio do CloudFormation, por algum motivo estranho. A handlerfunção é bastante auto-explicativa.

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 "Next"
  • Clique em "Criar Função"

Agora você pode testar a função lambda selecionando o botão "Teste" e selecione "Solicitação de criação do CloudFormation" como modelo de amostra. Você deve ver em seu log que as variáveis ​​alimentadas a ele são retornadas.

Use variável no seu modelo CloudFormation

Agora que temos essa função lambda, podemos usá-la nos modelos do CloudFormation. Primeiro, anote a função lambda Arn (vá para a página inicial lambda , clique na função recém-criada, o Arn deve estar no canto superior direito, algo assim 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]]]]

Primeiro, eu especifico uma Identityvariável que contém o Arn para a função lambda. Colocar isso em uma variável aqui significa que eu tenho que especificá-lo apenas uma vez. Eu faço todas as minhas variáveis ​​do tipo Custom::Variable. O CloudFormation permite que você use qualquer nome de tipo começando por Custom::para recursos personalizados.

Observe que a Identityvariável 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 Identityvariável, 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"]}). Crio 2 novas variáveis, cada uma com 2 campos: Nome e Arn. No restante do meu modelo, eu 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ê ficará preso por 1 a 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 ao desenvolver sua função lambda.

Claude
fonte
Resposta incrível! Eu o li e o executei nas minhas pilhas, embora, para mim, não me preocupo com a proliferação de funções lambda na minha conta e gosto de modelos independentes (modulo usando a cloudformation-toolgema), por isso empacoto a criação do lambda em o modelo e, em seguida, pode usá-lo diretamente em vez de criar o Identityrecurso personalizado. Veja aqui o meu código: gist.github.com/guss77/2471e8789a644cac96992c4102936fb3
Guss
Quando você está "... você fica preso entre 1 e 2 horas ..." porque um lambda travou e não respondeu com uma resposta cfn, você pode mover o modelo novamente usando manualmente curl / wget em o URL assinado. Certifique-se de sempre imprimir o evento / URL no início do lambda, para que você possa acessar o CloudWatch e obter o URL, se ele travar.
Taylor
12

Eu não tenho uma resposta, mas gostaria de salientar que você pode economizar muita dor usando Fn::Subno lugar deFn::Join

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

Substitui

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

Não. Eu tentei, mas fiquei vazio. A maneira que fez sentido para mim foi criar uma entrada de Mapeamentos chamada "CustomVariables" e ter essa casa com todas as minhas variáveis. Ele funciona para Strings simples, mas você não pode usar Intrinsics (Refs, Fn :: Joins, etc.) dentro de Mappings .

Trabalho:

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

Não vai funcionar:

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

Isso é apenas um exemplo. Você não colocaria uma referência independente em uma variável.

Roubar
fonte
1
A documentação diz que os valores de mapeamento devem ser cadeias literais.
Ivan Anishchuk
3

Você pode usar uma pilha aninhada que resolve todas as suas variáveis ​​em suas saídas e, em seguida, usar Fn::GetAttpara ler as saídas dessa pilha

SpoonMeiser
fonte
2

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

JoseOlcese
fonte