Große Wolken - CloudFormation Makros

Thumbnail

How to: CloudFormation Makro

CloudFormation vermisst gegenüber Terraform einige Funktionen, die das Erstellen von Infrastruktur vereinfachen können. Das ist grundsätzlich korrekt, allerdings gibt es in CloudFormation die Möglichkeit, sich selber um den Einbau solcher Funktionen zu kümmern. Das geht mithilfe sogenannter CloudFormation Makros.

CloudFormation Makros sind Funktionen, die wir per CloudFormation erstellen können und dann in weiteren CloudFormation Templates einbauen und verwenden können. Wir zeigen dies am Beispiel einer Count Funktion. Konkret wollen wir mithilfe von CloudFormation drei S3 Buckets erstellen mit gleicher Konfiguration. Diese S3 Buckets sollen von uns definierte Namen bekommen.

TL;DR

CloudFormation Makros sind Lambda Funktionen, an die das CloudFormation-Template weitergeleitet wird. Durch die Nutzung von Transformationen, wodurch die Lambda inkludiert wird, können eigene Funktionen genutzt werden.

Ausschnitt:

[...]
Transform:
  - Count
[...]

Der herkömmliche vs. der zukünftige Weg

Wir können natürlich ohne weitere Probleme mit herkömmlichen Methoden drei S3 Buckets mit gleicher Konfiguration erstellen. Das sieht dann z.B. folgendermaßen aus:

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Deploy 3 S3 Buckets.'

Resources:
############## S3 Buckets
  S3Bucket1:
    Type: AWS::S3::Bucket
    Properties: 
      BucketName: MyExampleBucket1

  S3Bucket2:
    Type: AWS::S3::Bucket
    Properties: 
      BucketName: MyExampleBucket2

  S3Bucket3:
    Type: AWS::S3::Bucket
    Properties: 
      BucketName: MyExampleBucket3

Das sind bereits 18 Zeilen für drei einfache S3 Buckets. Wir haben an dieser Stelle noch keine Konfiguration verwendet. Sonst hätten wir schnell 40 Zeilen. CloudFormation Templates werden schnell lang und dadurch unübersichtlich. Das muss doch besser gehen…

Unser Ziel ist also ein Template, welches folgendermaßen aussieht:

Transform:
  - Count
Resources:
  Bucket:
    Type: AWS::S3::Bucket
    Names:
      - myexamplebucket1
      - thisisthesecondbucket
      - threeisthewaytogo

Wir integrieren das Makro Count und geben eine Liste Names: an, welche die Namen der drei Buckets beinhaltet. Hier wollen wir so flexibel mit den Namen sein, wie die S3 Namesgebung es zulässt.

CloudFormation Makro: Count

Unser Ansatz ist die Verwendung eines CloudFormation Makros, welches wir Count nennen. Wir wollen also ein Bucket konfigurieren und eine Liste mit drei Namen dafür angeben. CloudFormation soll diese Liste erkennen und so viele Buckets erstellen, wie wir Namen in der Liste haben. Wir verwenden für die Lösung zwei CloudFormation Templates, wodurch wir die folgende kleine Architektur bauen:

Verwendung eines eigenen Makros

Wir erstellen in einem ersten Stack eine Lambda Funktion und machen diese als Makro verfügbar. In unserem zweiten Stack integrieren wir dieses Makro und verwenden es, um drei S3 Buckets mit wenigen Zeilen zu erstellen.

Unterstützung durch AWS

Zugegebenermaßen haben wir unsere Lösung nicht von Grund auf neu erfunden. Es gibt eine Implementierung von AWS dieser Count-Funktion auf awslabs. Hier gibt man einen Wert Count an, um zu bestimmen, wie viele Ressourcen einer Art erstellt werden sollen. Im Beispiel wird das mit S3 Buckets demonstriert. Allerdings kann man dabei nicht gänzlich individuelle Namen für die Buckets vergeben. Wir starten also mit dieser Lösung und passen sie an unsere Bedürfnisse an. Sie ist nicht als gänzlichen Ersatz für die Count-Funktion zu verstehen, aber zeigt wie eine solche Lösung adaptiert werden kann.

CloudFormation Ressource Makro (Macro)

Ein Makro ist nicht viel mehr als die Integration und der Aufruf einer Lambda-Funktion beim Ausrollen eines Stacks. Wir erstellen diese Ressource durch folgendes Template:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Count macro
  A simple iterator for creating multipledentical resources

Resources:
  Macro:
    Type: AWS::CloudFormation::Macro
    Properties:
      Name: Count
      FunctionName: !GetAtt CountMacroFunction.Arn
  CountMacroFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src
      Handler: index.handler
      Runtime: python3.6
      Timeout: 5

Das Template erstellt eine Lambda-Funktion und macht diese als Makro verfügbar. Dem Makro haben wir den Namen Count gegeben. Diesen Namen benötigen wir später, um das Makro in unseren anderen Templates mittels Transform verfügbar zu machen. Die Lambda-Funktion wird beim Ausrollen weiterer Templates aufgerufen, sofern diese das Makro Count verwenden. Wir können uns jetzt also auf den Code konzentrieren.

Lambda Code

Wir schreiben unsere Lambda-Funktion mit Python. Das Input-Event unserer Funktion beinhaltet das ganze Template, welches die Count Funktion integriert. Schauen wir also in den Code:

def process_template(template):
    new_template = copy.deepcopy(template)
    status = 'success'

    for name, resource in template['Resources'].items():
        print("resource = {}".format(resource))
        if 'Names' in resource:
            names = resource['Names']
            print("Found 'Names' property with length {} ....multiplying!".format(len(names)))            
            #Remove the original resource from the template but take a local copy of it
            resourceToMultiply = new_template['Resources'].pop(name)
            print("resourceToMultiply = {}".format(resourceToMultiply))
            #Create a new block of the resource multiplied with names ending in the iterator and the placeholders substituted
            resourcesAfterMultiplication = multiply(name, resourceToMultiply, names)
            print("resourcesAfterMultiplication = {}".format(resourcesAfterMultiplication))
            if not set(resourcesAfterMultiplication.keys()) & set(new_template['Resources'].keys()):
                new_template['Resources'].update(resourcesAfterMultiplication)
            else:
                status = 'failed'
                return status, template
        else:
            print("Did not find 'Count' property in '{}' resource....Nothing to do!".format(name))
    return status, new_template

Hier wird eine Kopie unseres eingehenden Templates erstellt. Wir übersetzen das eingehende Template in ein Template, welches CloudFormation ohne Makro lesen kann. new_template wird also das Template so beinhalten, wie es auf herkömmlichen Wege hätte erstellt werden müssen. Dieses Objekt wird später an CloudFormation zum Ausrollen zurückgegeben.

Wir suchen in dem Input-Template nach dem Key Names. Diesen Key verwenden wir in unserem Template und geben darin eine Liste an Namen für die S3 Buckets an (s.o.). Diese Liste übergeben wir an unsere multiply-Funktion:

def multiply(resource_name, resource_structure, count, names):
    resources = {}
    for name in names:
        resource_structure = {'Type': 'AWS::S3::Bucket'}
        print("Dealing with name = {}, resource_structure = {}".format(name, resource_structure)) 
        resource_structure['Properties'] = {}
        resource_structure['Properties']['BucketName'] = name
        resources[name] = resource_structure
        print("resource_structure = {}".format(resource_structure))
        print("resources = {}".format(resources))
    return resources

In dieser Funktion erstellen wir für jeden Namen aus der Liste namees eine neue resource_structure und fügen den Namen so ein, wie er in einem herkömmlichen Template stehen müsste.

def handler(event, context):
    print("Event = {}".format(event))
    result = process_template(event['fragment'])
    return {
        'requestId': event['requestId'],
        'status': result[0],
        'fragment': result[1],
    }

Unser handler macht nicht mehr, als dass er die process_template-Methode mit dem Template aufruft und das Ergebnis im richtigen Format zurück an CloudFormation übergibt. CloudFormation kann dann das Template auf herkömmliche Weise ausrollen.

Jetzt können wir die gewünschte gekürzte Version des CloudFormation Templates verwenden.

Fazit

CloudFormation Templates werden schnell lang. Wir haben durch wenig Aufwand einen Weg gefunden, wie wir unser Template verkürzen und übersichtlicher schreiben können. Besonders bei S3-Buckets, aber auch bei anderen Ressourcen ist die Konfiguration häufig die Gleiche.

Durch die präzise Angabe, dass unsere Namen im neuen Template ins Feld BucketName eingetragen werden, ist die Lösung aktuell nur für S3-Buckets verwendbar. Um die Count-Methode allgemeiner nutzen zu können, wird die Lambda durch eine Fallunterscheidung für verschiedene Ressourcen-Typen erweitert.

Mit Hilfe der Verwendung von Lambda-Funktionen sind den Makros keine Grenzen gesetzt. Jede Funktion, welche einem beim Erstellen eines Templates fehlt, kann eigenständig und individuell erstellt werden. Das macht CloudFormation gegenüber anderen IaC-Sprachen wieder konkurrenzfähig.

Auf awslabs findet ihr weitere vorgefertigte Makros von AWS.

Photo

Photo by Paul Skorupskas on Unsplash