web-dev-qa-db-fra.com

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

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 projets multi-AZ avec le nom ELB project et deux instances derrière l'ELB appelées project-1 et project-2. Je passe uniquement le paramètre ELBHostName 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 attribuer la sortie de ce Fn::Join à une variable quelconque et ne fait référence qu'à cela, tout comme je peux avec "Ref": déclaration.

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?

20
MLu

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 é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 variables uniquement (quelque chose que je dirais qui aurait dû être dans CloudFormation en premier lieu), mais cela 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 1 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 il a également besoin d'un rôle.

Créer une fonction lambda

  • Allez sur page d'accueil 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 cfn-response python module , qui ne s'auto-installe que si la fonction lambda est créée via CloudFormation, pour une raison étrange. La fonction handler 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, 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 variable Identity 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 les ressources personnalisées.

Notez que la variable Identity 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 variable Identity, je peux définir de nouvelles variables en utilisant ServiceToken: !GetAtt [Identity, Arn] (Je pense que le code JSON devrait ressembler à "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 utiliser !GetAtt [ClientBucketVar, Name] ou !GetAtt [ClientBucketVar, Arn] chaque fois que 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.

5
Claude

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

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

Remplace

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

Non, je l'ai essayé, mais est venu vide. La façon qui me paraissait logique était de créer une entrée de mappages 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.) à l'intérieur des 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.

3
Rob

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

3
SpoonMeiser

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.

2
JoseOlcese