AWS CloudFormation - Variables personnalisées dans les modèles

18

Existe-t-il un moyen de définir des raccourcis pour les valeurs fréquemment utilisées dérivées des paramètres du modèle CloudFormation?

Par exemple - J'ai un script qui crée une pile de projet Multi-AZ avec le nom ELB projectet deux instances derrière l'ELB appelées project-1et project-2. Je passe uniquement le ELBHostNameparamètre au modèle et plus tard je l'utilise pour construire:

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

Cette construction ou très similaire est répétée plusieurs fois dans le modèle - pour créer le nom d'hôte EC2, les enregistrements Route53, etc.

Au lieu de répéter cela encore et encore, je voudrais affecter la sortie de cela Fn::Joinà une variable quelconque et ne me référer qu'à cela, tout comme je le peux avec l' "Ref":instruction.

Idéalement, quelque chose comme:

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

ou quelque chose de similaire simple.

Est-ce possible avec Amazon CloudFormation?

MLu
la source
ELBHostName full est-il un paramètre que vous passez explicitement à Cloudformation? Si oui, pourquoi utiliser une référence? Pourrait utiliser Moustache pour inclure des variables dans votre modèle et le transformer en JSON avant de l'envoyer à Cloudformation. Dépend à quoi ressemble votre processus de provisioning.
Canuteson

Réponses:

5

Je cherchais la même fonctionnalité. L'utilisation d'une pile imbriquée comme SpoonMeiser l'a suggéré m'est venue à l'esprit, mais j'ai réalisé que ce dont j'avais réellement besoin, c'était de fonctions personnalisées. Heureusement, CloudFormation permet d'utiliser AWS :: CloudFormation :: CustomResource qui, avec un peu de travail, permet de faire exactement cela. Cela semble exagéré pour les seules variables (quelque chose que je dirais qui aurait dû être dans CloudFormation en premier lieu), mais il fait le travail et, en plus, permet toute la flexibilité de (faites votre choix de python / nœud /Java). Il convient de noter que les fonctions lambda coûtent de l'argent, mais nous parlons ici de quelques centimes, sauf si vous créez / supprimez vos piles plusieurs fois par heure.

La première étape consiste à créer une fonction lambda sur cette page qui ne fait que prendre la valeur d'entrée et la copier dans la sortie. Nous pourrions avoir la fonction lambda faire toutes sortes de choses folles, mais une fois que nous avons la fonction d'identité, tout le reste est facile. Alternativement, nous pourrions avoir la fonction lambda en cours de création dans la pile elle-même. Étant donné que j'utilise plusieurs piles dans un compte, j'aurais tout un tas de fonctions et de rôles lambda restants (et toutes les piles doivent être créées avec --capabilities=CAPABILITY_IAM, car elles ont également besoin d'un rôle.

Créer une fonction lambda

  • Accédez à la page d'accueil de lambda et sélectionnez votre région préférée
  • Sélectionnez "Fonction vierge" comme modèle
  • Cliquez sur "Suivant" (ne configurez aucun déclencheur)
  • Remplir:
    • Nom: CloudFormationIdentity
    • Description: retourne ce qu'il obtient, prise en charge variable dans Cloud Formation
    • Runtime: python2.7
    • Type d'entrée de code: Modifier le code en ligne
    • Code: voir ci-dessous
    • Gestionnaire: index.handler
    • Rôle: créez un rôle personnalisé. À ce stade, une fenêtre contextuelle s'ouvre qui vous permet de créer un nouveau rôle. Acceptez tout sur cette page et cliquez sur "Autoriser". Il créera un rôle avec des autorisations pour publier dans les journaux cloudwatch.
    • Mémoire: 128 (c'est le minimum)
    • Délai d'attente: 3 secondes (devrait être suffisant)
    • VPC: aucun VPC

Copiez-collez ensuite le code ci-dessous dans le champ de code. Le haut de la fonction est le code du module python cfn-response , qui n'est installé automatiquement que si la fonction lambda est créée via CloudFormation, pour une raison étrange. La handlerfonction est assez explicite.

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")
  • Cliquez sur Suivant"
  • Cliquez sur "Créer une fonction"

Vous pouvez maintenant tester la fonction lambda en sélectionnant le bouton "Test" et sélectionnez "CloudFormation Create Request" comme exemple de modèle. Vous devriez voir dans votre journal que les variables qui y sont alimentées sont retournées.

Utiliser une variable dans votre modèle CloudFormation

Maintenant que nous avons cette fonction lambda, nous pouvons l'utiliser dans les modèles CloudFormation. Prenez d'abord note de la fonction lambda Arn (allez sur la page d'accueil lambda , cliquez sur la fonction qui vient d'être créée, l'Arn devrait être en haut à droite, quelque chose comme arn:aws:lambda:region:12345:function:CloudFormationIdentity).

Maintenant, dans votre modèle, dans la section des ressources, spécifiez vos variables comme:

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]]]]

Je spécifie d'abord une Identityvariable qui contient l'Arn pour la fonction lambda. Mettre ceci dans une variable ici, signifie que je n'ai qu'à le spécifier une fois. Je fais toutes mes variables de type Custom::Variable. CloudFormation vous permet d'utiliser n'importe quel nom de type commençant par Custom::pour des ressources personnalisées.

Notez que la Identityvariable contient deux fois Arn pour la fonction lambda. Une fois pour spécifier la fonction lambda à utiliser. La deuxième fois comme valeur de la variable.

Maintenant que j'ai la Identityvariable, je peux définir de nouvelles variables en utilisant ServiceToken: !GetAtt [Identity, Arn](je pense que le code JSON devrait être quelque chose comme "ServiceToken": {"Fn::GetAtt": ["Identity", "Arn"]}). Je crée 2 nouvelles variables, chacune avec 2 champs: Name et Arn. Dans le reste de mon modèle, je peux l'utiliser !GetAtt [ClientBucketVar, Name]ou !GetAtt [ClientBucketVar, Arn]quand j'en ai besoin.

Un mot d'avertissement

Lorsque vous travaillez avec des ressources personnalisées, si la fonction lambda se bloque, vous êtes bloqué entre 1 et 2 heures, car CloudFormation attend une réponse de la fonction (bloquée) pendant une heure avant d'abandonner. Par conséquent, il peut être utile de spécifier un court délai d'attente pour la pile lors du développement de votre fonction lambda.

Claude
la source
Réponse géniale! Je l'ai lu et l'ai exécuté dans mes piles, mais pour moi, je ne m'inquiète pas de la prolifération des fonctions lambda dans mon compte et j'aime les modèles autonomes (je modularise à l'aide de la cloudformation-toolgemme), donc j'emballe la création lambda dans le modèle et peut ensuite l'utiliser directement au lieu de créer la Identityressource personnalisée. Voir ici pour mon code: gist.github.com/guss77/2471e8789a644cac96992c4102936fb3
Guss
Lorsque vous êtes "... vous êtes bloqué entre 1 et 2 heures ..." parce qu'un lambda s'est écrasé et n'a pas répondu avec une réponse cfn, vous pouvez faire repartir le modèle en utilisant manuellement curl / wget sur l'URL signée. Assurez-vous simplement d'imprimer toujours l'événement / l'URL au début du lambda afin de pouvoir accéder à CloudWatch et obtenir l'URL si elle se bloque.
Taylor
12

Je n'ai pas de réponse, mais je voulais souligner que vous pouvez vous épargner beaucoup de douleur en utilisant Fn::Subà la place deFn::Join

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

Remplace

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

Non, je l'ai essayé, mais est venu vide. La manière qui me paraissait logique était de créer une entrée Mappings appelée "CustomVariables" et de faire en sorte qu'elle héberge toutes mes variables. Cela fonctionne pour les chaînes simples, mais vous ne pouvez pas utiliser Intrinsics (Refs, Fn :: Joins, etc.) dans les mappages .

Travaux:

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

Ne fonctionnera pas:

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

Ce n'est qu'un exemple. Vous ne mettriez pas une référence autonome dans une variable.

Rob
la source
1
La documentation indique que les valeurs de mappage doivent être des chaînes littérales.
Ivan Anishchuk
3

Vous pouvez utiliser une pile imbriquée qui résout toutes vos variables dans ses sorties, puis utiliser Fn::GetAttpour lire les sorties de cette pile

SpoonMeiser
la source
2

Vous pouvez utiliser des modèles imbriqués dans lesquels vous "résolvez" toutes vos variables dans le modèle externe et les transmettez à un autre modèle.

JoseOlcese
la source