AWS CloudFormation - Benutzerdefinierte Variablen in Vorlagen

18

Gibt es eine Möglichkeit, Verknüpfungen für häufig verwendete Werte zu definieren, die aus CloudFormation-Vorlagenparametern abgeleitet wurden?

Zum Beispiel - Ich habe ein Skript, das einen Multi-AZ-Projektstapel mit dem ELB-Namen projectund zwei Instanzen hinter dem ELB mit dem Namen project-1und erstellt project-2. Ich übergebe nur ELBHostNameParameter an die Vorlage und benutze sie später zum Konstruieren:

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

Diese oder eine sehr ähnliche Konstruktion wird in der gesamten Vorlage mehrmals wiederholt, um den EC2-Hostnamen, Route53-Datensätze usw. zu erstellen.

Anstatt das immer wieder zu wiederholen, möchte ich die Ausgabe Fn::Joineiner Variablen zuweisen und nur darauf verweisen, so wie ich es mit "Ref":Anweisung tun kann .

Idealerweise so etwas wie:

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

oder etwas ähnlich Einfaches.

Ist das mit Amazon CloudFormation möglich?

MLu
quelle
Ist ELBHostName ein Parameter, den Sie explizit an Cloudformation übergeben? Wenn ja, warum einen Ref verwenden? Könnte Moustache verwenden, um Variablen in Ihre Vorlage aufzunehmen und diese in JSON umzuwandeln, bevor sie an Cloudformation gesendet werden. Hängt davon ab, wie der Bereitstellungsprozess aussieht.
Canuteson

Antworten:

5

Ich habe nach der gleichen Funktionalität gesucht. Es kam mir in den Sinn, einen verschachtelten Stapel zu verwenden, wie es SpoonMeiser vorschlug, aber dann wurde mir klar, dass ich tatsächlich benutzerdefinierte Funktionen benötigte. Zum Glück ermöglicht CloudFormation die Verwendung von AWS :: CloudFormation :: CustomResource , mit der man mit ein wenig Arbeit genau das tun kann. Dies fühlt sich wie ein Overkill für nur Variablen an (etwas, von dem ich behaupte, dass es ursprünglich in CloudFormation gewesen sein sollte), aber es erledigt die Aufgabe und ermöglicht darüber hinaus die gesamte Flexibilität von (wählen Sie Python / Node aus) /Java). Es sollte beachtet werden, dass Lambda-Funktionen Geld kosten, aber wir sprechen hier von Pennies, es sei denn, Sie erstellen / löschen Ihre Stapel mehrmals pro Stunde.

Der erste Schritt besteht darin, auf dieser Seite eine Lambda-Funktion zu erstellen , die nichts anderes tut, als den Eingabewert zu übernehmen und in die Ausgabe zu kopieren. Wir könnten die Lambda-Funktion alle möglichen verrückten Sachen machen lassen, aber sobald wir die Identitätsfunktion haben, ist alles andere einfach. Alternativ könnte die Lambda-Funktion im Stapel selbst erstellt werden. Da ich viele Stapel in einem Konto verwende, hätte ich eine ganze Reihe von Lambda-Funktionen und -Rollen übrig (und alle Stapel müssen mit erstellt werden --capabilities=CAPABILITY_IAM, da sie auch eine Rolle benötigen.

Lambda-Funktion erstellen

  • Gehen Sie zur Lambda-Homepage und wählen Sie Ihre Lieblingsregion aus
  • Wählen Sie als Vorlage "Blank Function"
  • Klicken Sie auf "Weiter" (keine Trigger konfigurieren)
  • Ergänze:
    • Name: CloudFormationIdentity
    • Beschreibung: Gibt zurück, was es bekommt, variable Unterstützung in Cloud Formation
    • Laufzeit: python2.7
    • Code Entry Type: Code Inline bearbeiten
    • Code: siehe unten
    • Handler: index.handler
    • Rolle: Erstellen Sie eine benutzerdefinierte Rolle. An dieser Stelle wird ein Popup geöffnet, in dem Sie eine neue Rolle erstellen können. Akzeptiere alles auf dieser Seite und klicke auf "Zulassen". Es wird eine Rolle mit Berechtigungen zum Posten in Cloudwatch-Protokollen erstellt.
    • Speicher: 128 (dies ist das Minimum)
    • Timeout: 3 Sekunden (sollte ausreichend sein)
    • VPC: Keine VPC

Dann kopieren Sie den unten stehenden Code und fügen ihn in das Codefeld ein. Die Spitze der Funktion ist der Code aus dem cfn-response-Python-Modul , der aus irgendeinem Grund nur dann automatisch installiert wird, wenn die Lambda-Funktion über CloudFormation erstellt wird. Die handlerFunktion ist ziemlich selbsterklärend.

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")
  • Weiter klicken"
  • Klicken Sie auf "Funktion erstellen"

Sie können nun die Lambda-Funktion testen, indem Sie auf die Schaltfläche "Test" klicken und "CloudFormation Create Request" als Beispielvorlage auswählen. Sie sollten in Ihrem Protokoll sehen, dass die ihm zugeführten Variablen zurückgegeben werden.

Verwenden Sie eine Variable in Ihrer CloudFormation-Vorlage

Nachdem wir diese Lambda-Funktion haben, können wir sie in CloudFormation-Vorlagen verwenden. Notieren Sie sich zunächst die Lambda-Funktion Arn (gehen Sie zur Lambda-Homepage , klicken Sie auf die gerade erstellte Funktion, die Arn sollte sich etwa oben rechts befinden arn:aws:lambda:region:12345:function:CloudFormationIdentity).

Geben Sie nun in Ihrer Vorlage im Ressourcenbereich Ihre Variablen an, wie z.

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

Zuerst gebe ich eine IdentityVariable an, die das Arn für die Lambda-Funktion enthält. Wenn ich das hier in eine Variable setze, muss ich es nur einmal angeben. Ich mache alle meine Variablen vom Typ Custom::Variable. Mit CloudFormation können Sie jeden Typnamen verwenden, der mit Custom::benutzerdefinierten Ressourcen beginnt .

Beachten Sie, dass die IdentityVariable zweimal das Arn für die Lambda-Funktion enthält. Einmal, um die zu verwendende Lambda-Funktion anzugeben. Das zweite Mal als Wert der Variablen.

Nun, da ich die IdentityVariable habe, kann ich neue Variablen mit definieren ServiceToken: !GetAtt [Identity, Arn](ich denke, JSON-Code sollte so ähnlich sein "ServiceToken": {"Fn::GetAtt": ["Identity", "Arn"]}). Ich erstelle 2 neue Variablen mit jeweils 2 Feldern: Name und Arn. Im Rest meiner Vorlage kann ich !GetAtt [ClientBucketVar, Name]oder !GetAtt [ClientBucketVar, Arn]wann immer ich es brauche.

Achtung!

Wenn Sie mit benutzerdefinierten Ressourcen arbeiten und die Lambda-Funktion abstürzt, müssen Sie zwischen 1 und 2 Stunden warten, da CloudFormation eine Stunde lang auf eine Antwort der (abgestürzten) Funktion wartet, bevor Sie aufgeben. Daher kann es sinnvoll sein, während der Entwicklung Ihrer Lambda-Funktion eine kurze Zeitüberschreitung für den Stack festzulegen.

Claude
quelle
Geniale Antwort! Ich habe es gelesen und in meinen Stapeln abgelegt, aber ich mache mir keine Sorgen über die Verbreitung von Lambda-Funktionen in meinem Konto und ich mag Vorlagen, die eigenständig sind (ich modularisiere sie mit dem cloudformation-toolGem), also packe ich die Lambda-Erstellung in die Vorlage und kann sie dann direkt verwenden, anstatt die Identitybenutzerdefinierte Ressource zu erstellen . Siehe hier für meinen Code: gist.github.com/guss77/2471e8789a644cac96992c4102936fb3
Guss
Wenn Sie "... zwischen 1 und 2 Stunden festsitzen ...", weil ein Lambda abgestürzt ist und nicht mit einer cfn-Antwort geantwortet hat, können Sie die Vorlage durch manuelles Verwenden von curl / wget on wieder in Bewegung setzen die signierte URL. Stellen Sie einfach sicher, dass Sie das Ereignis / die URL immer am Anfang des Lambdas ausdrucken, damit Sie zu CloudWatch gehen und die URL abrufen können, falls sie hängen bleibt.
Taylor
12

Ich habe keine Antwort, wollte aber darauf hinweisen, dass Sie sich viel Schmerz ersparen können, indem Sie Fn::Subanstelle vonFn::Join

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

Ersetzt

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

Nein, ich habe es versucht, kam aber leer. Für mich war es sinnvoll, einen Mappings-Eintrag mit dem Namen "CustomVariables" zu erstellen und alle meine Variablen in diesem Haus zu haben. Es funktioniert für einfache Strings, aber Sie können Intrinsics (Refs, Fn :: Joins usw.) nicht in Mappings verwenden .

Werke:

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

Wird nicht funktionieren:

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

Das ist nur ein Beispiel. Sie würden keinen eigenständigen Verweis in eine Variable einfügen.

rauben
quelle
1
Die Dokumentation besagt, dass Zuordnungswerte Literalzeichenfolgen sein müssen.
Ivan Anishchuk
3

Sie können einen verschachtelten Stapel verwenden, der alle Ihre Variablen in seinen Ausgaben auflöst und dann Fn::GetAttzum Lesen der Ausgaben von diesem Stapel verwendet

SpoonMeiser
quelle
2

Sie können verschachtelte Vorlagen verwenden, in denen Sie alle Variablen in der äußeren Vorlage "auflösen" und an eine andere Vorlage übergeben.

JoseOlcese
quelle