//

Infrastructure as Code in AWS: Keine Silver Bullet

13.12.2022 | 26 Minuten Lesezeit

TL;DR

Es gibt keine Universalmethode. Infrastructure as Code ist ein vergleichsweise neuer Ansatz. Einige Lösungen rund um Infrastructure as Code befinden sich noch in der Entwicklung. Es gibt keinen klaren Favoriten. Die Wahl des passenden Tools hängt daher von dem eigenen Kontext sowie den Eigenschaften des Tools ab.

Schnelle Hilfe?

  • Stabile API? AWS CloudFormation!
  • Skalierende Infrastruktur? AWS Cloud Develoment Kit!
  • Cross-Region/Cross-Account-Abhängigkeiten? Terraform!

Einleitung

Dieser Artikel beschäftigt sich mit den Infrastructure-as-Code-(IaC-)Tools CloudFormation, Cloud Development Kit und Terraform. Hierbei legen wir das Augenmerk auf Resource Deployments in die AWS Cloud. Tools für andere Cloud-Provider sowie deren Eigenschaften lassen wir im Rahmen dieses Artikels außen vor. Neben einer kurzen Erklärung zu jedem Tool stellen wir markante Detailunterschiede heraus. Wir vergleichen die Tools anhand von Merkmalen wie Vollständigkeit, Erweiterbarkeit, Testbarkeit.

Ziel des Artikels ist es, dir als Leser eine Entscheidungsgrundlage für die Wahl eines passenden IaC-Tools an die Hand zu geben. CloudFormation, Cloud Development Kit und Terraform sind die bisher gängigsten Werkzeuge in unseren Projektumfeldern gewesen. Es gibt auch weitere IaC-Tools wie Pulumi, Severless Application Model (SAM) oder Ansible. Diese beleuchten wir in diesem Artikel jedoch nicht, da wir entweder bisher noch keine Erfahrung sammeln konnten oder das Tool das Erstellen von Cloud-Ressourcen nicht direkt im Fokus hat.

IaC Basics

IT-Infrastruktur für komplexe Use Cases in der Cloud manuell zu managen wird immer schwieriger bis gar unmöglich. Aus diesem Grund vereinfachen Cloud-Provider diese Tätigkeiten mithilfe von Automatisierung. Entweder werden Services automatisiert über CLI-Skripte erstellt und angepasst, oder es wird ein Infrastructure as Code-Tool eingesetzt. IaC-Tools ermöglichen es, Ressourcen deklarativ zu erstellen oder zu ändern. Die Konfigurationen der Services werden mit ihrer Konfiguration in einem Template abgelegt. Schliesslich baut das Tool basierend auf dieser Konfiguration die jeweilige Ressource in der Cloud auf. Die Konfigurationsdateien können je nach Tool in Markup- oder Programmiersprachen verfasst und versioniert werden.

Meist werden Service-Konfigurationen logisch, fachlich oder technisch gruppiert und zusammen deployt. Diese Bündelung von Ressourcen werden Projekte oder auch Stacks genannt. Ein Stack kann beispielsweise virtuelle Maschinen für eine Webanwendung, deren Netzwerk-Routen und einen Datenhaltungsservice umfassen. Dadurch kann der Use Case vollständig gekapselt abgebildet werden.

Nachdem die Ressourcen in der Cloud aufgebaut wurden, speichert sich das IaC-Tool den aktuellen Status in einem Statefile ab. Dieses macht Metadaten nachverfolgbar und verbessert die Performance für große Infrastrukturen.

CloudFormation

CloudFormation ist ein von AWS bereitgestellter Dienst mit einem stabilen Reifegrad. AWS selbst nutzt CloudFormation in vielen Services unter der Haube. Nennenswerte Beispiele sind hier der AWS Control Tower oder auch AWS Beanstalk. Die Nutzung mit AWS-Services ist überwiegend kostenfrei.

Infrastruktur wird in JSON- oder YAML-Dateien mit einem eigenen Template geschrieben. CloudFormation bietet einen großen Umfang an APIs und Möglichkeiten für aktuelle AWS-Services. Neue AWS-Service- oder Feature-Announcements liefern meist direkt zum Release oder kurzzeitig später eine CloudFormation-Anbindung. Dennoch ist es empfohlen, vor der Nutzung eines neuen Services in der API-Referenz nachzusehen, ob auch entsprechende APIs vorhanden sind. Für kleinere Services wie WorkMail gibt es bspw. nur wenige bis gar keine Optionen für CloudFormation. Organisationsmanagement hätten wir an dieser Stelle auch als Beispiel für fehlende Schnittstellen erwähnt, es wurde aber kurz vor dem Erstellen dieses Artikels durch ein neues Feature ermöglicht.

Das bereits erwähnte Statefile hält der AWS CloudFormation Service vor. Als Nutzer wird es für uns gemanagt. Wir müssen uns daher um das Hosting keine Gedanken machen, können aber auch nicht direkt darauf zugreifen.

Regionaler Scope

CloudFormation ist regionsspezifisch. Dies bedeutet, dass alle angegebenen Ressourcen in einer spezifischen Region aufgebaut werden. Ein Stack, der Ressourcen über mehrere Regionen verteilt, ist vom Grundkonzept her nicht vorgesehen. Die Verkettung von Ressourcen zwischen Regionen stellt sich dann oft als größere Herausforderung dar. Zumindest existiert die Option, ein Stack-Template in mehreren Regionen oder Accounts gleichzeitig anzulegen. Hierbei handelt es sich um das Feature der StackSets. Beispielsweise können hier IAM Permissions in einem Template in mehrere Umgebungen gleichzeitig ausgerollt werden. Dies spart zusätzlichen manuellen Scripting-Aufwand.

Innerhalb einer Region gibt es die Möglichkeit, über CloudFormation Output-Parameter oder über weitere AWS-Services (Systems-Manager) zwischen Stacks Informationen auszutauschen. Auf Informationen anderer Regionen lässt sich ohne größeren Eigenaufwand nicht so leicht zugreifen.

Beispiele sind hier:

  • CloudFront Deployment außerhalb von us-east-1: Der globale CDN-Dienst CloudFront kann von jeder Region aus deployt werden. Soll bei CloudFront ein Zertifikat, WAF oder Lambda@Edge integriert werden, kann der Service nur Ressourcen aus us-east-1 einbinden. Wie kommt nun CloudFront an die Information, welches Zertifikat es nutzen soll? Eine Referenzierung mittels CloudFormation APIs ist nicht möglich.
  • Route 53 (DNS) Cross-Region- oder Cross-Account-Setup: Die Verbindung zweier DNS-Server per DNZ-Zone-Delegation in unterschiedlichen Accounts oder Regionen stellt eine Herausforderung bei der Zone-Delegation dar. Die Verknüpfung der Nameserver-Einträge muss entweder manuell nachgetragen oder selber implementiert werden.

Custom Resource

Wenn wir manuelles Skripten vor oder nach einem Deployment so weit wie möglich vermeiden wollen, aber dennoch eine regionsübergreifende Abhängigkeit vorfinden, gibt es das Feature einer Custom Resource.

Diese Custom-Resource-Schnittstelle ermöglicht es, eigene APIs aufzurufen. Hier können wir beispielsweise eine Lambda-Funktion triggern. Mit dieser Lambda-Funktion können wir unsere eigene Logik einbringen. Wenn wir hier Ressourcen in anderen Regionen anlegen wollen, müssen wir natürlich neben dem Erstellen der Ressource auch Updates und das Löschen der Ressourcen in der Lambda-Funktion programmieren. Dies ist jedoch mit Aufwand verbunden, den wir uns mithilfe des IaC-Tools ersparen wollten. Daher sollten wir uns im Vorhinein gut informieren, dass möglichst alles von CloudFormation selber unterstützt wird.

Maintenance

Dank einer stabilen API und fehlender Abhängigkeiten innerhalb einzelner Stack-Templates ist der Maintenance-Aufwand der Konfigurationsdateien sehr niedrig. Auch die Syntax und Template Anatomy ist sehr stabil.

Testing

Die erstellten Konfigurationen werden experimentell getestet, indem diese in einen AWS-Account deployt werden. Nun können die angelegten Ressourcen auf den gewünschten Zustand überprüft werden. Eine Test-Automatisierung ist hier nicht ohne Weiteres möglich. Wichtig zu wissen ist, dass die ausgerollten Ressourcen je nach Service nun auch Kosten verursachen können!

Limits

CloudFormation ist technisch in der Lage, mit vielen Ressourcen innerhalb eines Stacks umzugehen (500 Ressourcen innerhalb eines Stacks & 2000 Stacks per Account). Bevor wir jedoch in die technischen Limits laufen, ist das YAML- oder JSON-Template so gewachsen, dass die Verständlichkeit leidet und Ressourcen oder Verknüpfungen immer schwieriger nachvollziehbar sind. Aus diesem Grund hat es sich etabliert, möglichst kleine Stacks aufzubauen. Ist dies nicht so einfach, gehen viele auf eine weitere Abstraktionsebene und werfen einen Blick auf das Cloud Development Kit.

Cloud Development Kit

Mit immer mehr Infrastruktur wird es immer schwieriger, CloudFormation-Templates zu pflegen. Die CloudFormation-Templates stellen zwar die Konfiguration der Ressourcen dar, jedoch sind sie nicht darauf ausgelegt, die Ideen hinter der Architektur zu erfassen. Beispiele dafür sind logische Einheiten, Beziehungen, bewährte Verfahren oder wiederkehrende Muster. Oft finden wir uns bei häufigem Kopieren von CloudFormation-Template-Fragmenten wieder. Es werden schnell gewisse Stellen über mehrere Ressourcen oder Stacks beim Überarbeiten übersehen. Auch ist es schwierig, die Templates mithilfe von Tests zu validieren, bevor sie deployt werden. All diese Probleme lösen wir Entwickler historisch durch den Einsatz von Programmiersprachen. Aus diesem Grund ist die Idee entstanden, Infrastruktur anhand von Programmiersprachen aufbauen zu lassen. Das AWS Cloud Development Kit (aws-cdk) ist ein Open-Source-Projekt von AWS, das eine Abstraktion auf CloudFormation aufbaut, die wiederum genau das ermöglicht. Dank der Open-Source-Community ist sehr viel Wissen innerhalb der GitHub Issues zu finden. Hier sind auch sogenannte Tracking Issues zu finden. Sie zeigen den Progress zu den jeweiligen Themen auf.

Dank der Abstraktionen des Cloud Development Kits sind wir in der Lage, mit ein paar Zeilen Code Infrastruktur aufzubauen, die in CloudFormation mehrere hunderte Zeilen umfasst. Im Grundsatz nutzten wir hier eine Programmiersprache, mit der ein CloudFormation-Template generiert wird.

Das Cloud Development Kit selber ist in TypeScript geschrieben. Dank des jsii-Projekts ist es möglich, statt TypeScript verschiedene andere Sprachen wie Python, Java, C# oder Go zur Programmierung der Infrastruktur zu nutzen.

Concepts

Das Cloud Development Kit bietet ein modulares System. Konstrukte (Constructs) sind die Grundbausteine eines Cloud-Development-Kit-Projekts. Das Konstrukt repräsentiert eine Cloud-Komponente und kapselt alles, was CloudFormation zum Erstellen der Komponente benötigt.

Es gibt drei verschiedene Layer an Konstrukten.

  1. Bei dem ersten und primitivsten Layer 1 (L1) handelt es sich rein um ein generiertes Abbild der CloudFormation-API. Dies bedeutet, dass alles, was in CloudFormation möglich ist, auch mit dem Cloud Development Kit aufgebaut werden kann.
  2. Das nächste Level, die L2-Konstrukte, bietet eine API basierend auf Absichten. Es kapselt Defaults, Boilerplates und Glue-Logik, die man mit L1-Konstrukten sonst selber schreiben müsste. Darüber hinaus bietet es komfortable Methoden, die die Arbeit mit Ressourcen vereinfacht. Aktuell ist ein großer Umfang an AWS-Services mit L2-Konstrukten versehen. Dennoch gibt es noch eine Liste an Services, die sich aktuell in einem Alpha-Release befinden. Vorteilhaft ist hier, dass alles Open Source ist, sodass der aktuelle Arbeitsstand eingesehen, Feedback gegeben oder über Contributions mitgeholfen werden kann. Insgesamt wird bereits eine breite Masse an Use Cases unterstützt. Dennoch gibt es noch eine Reihe an Edge Cases, die sich im Research-Status befinden.
  3. Die L3-Konstrukte nennen die CDK-Entwickler Patterns. Sie enthalten mehrere Arten von Ressourcen und sind dafür da, gesamte wiederverwendbare Tasks abzubilden.

Wichtig zu erwähnen ist, dass L2- und L3-Konstrukte miteinander kombiniert werden können. Möglich machen dies die in L2 eingebauten Helper-Methoden. Da diese in L1 jedoch nicht verfügbar sind, können L1-Konstrukte nicht direkt mit anderen Konstrukten kombiniert werden.

Integrations

Sehr praktisch ist, dass neben dem Beschreiben der Infrastruktur in einer Programmiersprache auch weitere Integrationen möglich sind. Beispielsweise bietet es neben dem reinen aws-lambda-Modul auch ein aws-lambda-nodejs- oder auch aws-lambda-python-Modul. Diese Module haben neben der reinen Lambda-Ressource auch das Bundling des Lambda-Codes mit eingebaut. Im Fall von Node.js kompiliert es den TypeScript-Code zu JavaScript, installiert alle benötigten Dependencies und deployt dieses Bundle als Lambda-Funktion. Dies erspart einiges an Arbeitsaufwand und es wird kein eigenes Tooling mehr benötigt.

Darüber hinaus gibt es nun auch eine Lösung des im CloudFormation angesprochenen Problem beim Deployment einer CloudFront-Distribution. Das Cloud Development Kit bietet ein Konstrukt, das das Anlegen eines Zertifikats in einer anderen Region vornimmt. Unter der Haube deployt es eine Lambda und ruft diese mittels einer CustomResource beim Deployment auf. Auch der benötigte Lambda-Handler-Code wird bereits mitgeliefert.

Requirements

Um das Cloud Development Kit in vollem Umfang nutzen zu können, benötigt dieses neben dem Zugang zu CloudFormation auch noch weitere Ressourcen, um Metadaten abzulegen. Hierzu müssen Hilfsressourcen wie unter anderem ein S3 Bucket zur Ablage der Metadaten aufgebaut werden. Dieser Prozess zum Aufsetzen in einer Region wird Bootstrapping genannt. Es existiert der cdk bootstrap-Befehl, der alle nötigen Ressourcen über ein CloudFormation-Template in der gewünschten Region deployt. Möchten wir mit dem Cloud Development Kit in unterschiedliche Accounts oder Regionen deployen, müssen alle jeweils vorher einmal gebootstrapped werden. Gibt es spezifischere Anforderungen an dieses Setup, kann das Template manuell für sich angepasst werden.

Maintenance

Da sich das Cloud Development Kit im Node.js-Umfeld bewegt, existieren einige Abhängigkeiten zu anderen Bibliotheken. Neben regelmäßigen Releases, die viele neue Features bringen, müssen auch weitere Abhängigkeiten regelmäßig aktualisiert werden. Dies bedeutet, dass ein gewisser Wartungsaufwand entsteht. Mithilfe von Dependency-Automatisierung können wir versuchen, den Aufwand so klein wie möglich zu halten. Dennoch existiert das Risiko – anders als beim stabilen CloudFormation – dass gewisser Maintenance-Aufwand entsteht, wenn Infrastruktur-Anpassungen nach einiger Zeit vorgenommen werden.

Custom Resource

Enthält der Use-Case eine Abhängigkeit zu einer Ressource in einer anderen Region, kann auch hier zur Custom Resource-Schnittstelle von CloudFormation gegriffen werden. Das Cloud Development Kit bringt hier bereits eine *Custom-Resource-*Komponente mit, die das Erstellen einer Custom-Resource vereinfacht. Dies ist ein großer Vorteil, da das Cloud Development Kit mit dieser Komponente das Heavy-Lifting abnimmt und nur noch die jeweilige Create-, Update- oder Delete-Logik entwickelt werden muss.

Kompatibilität

Um eine Kompatibilität zu CloudFormation-Templates zu schaffen, gibt es das cloudformation_include-Modul. Es ermöglicht das Einbeziehen von CloudFormation-Templates.

Testing

Ein weiteres Ziel, das sich das Team hinter dem Cloud Development Kits gesteckt hat, ist die Testbarkeit der Ressourcen, bevor ein Deployment ausgeführt wird. Hierzu existiert das assertions-Modul. Es bietet die Möglichkeit, Snapshot-Tests durchzuführen oder spezielle Checks innerhalb des generierten CloudFormation-Templates. Diese helfen, unbeabsichtigte Änderungen im CloudFormation-Template mitzubekommen oder absichtliche Änderungen genau validieren zu können. Neben den Snapshot-Tests gibt es auch die Möglichkeit feingranularer Validierungen. Dies ermöglicht es, Security- oder Compliance-Checks auf dem Template durchzuführen. Beispielsweise kann man überprüfen, dass eine Lambda-Funktion in einem Konstrukt in ein bestimmtes VPC gehängt wird oder eine neu erstellte Rolle eine Permission-Boundary enthält. Mit solchen Checks kann schon vor dem Deployment potenzielle Fehlerquellen abfangen. Dies schafft einen schnellen Feedback Loop für die Entwickler.

Berechtigungen

Alle Abstraktionen sind sehr angenehm in der Entwicklung. Wenn es jedoch um das Thema Deployment Role für ein Least-Privilege-Deployment auf Produktion geht, müssen IAM-Permission-Sets auf der API-Requests-Ebene aufgebaut werden. Durch all die Abstraktionen und Kapselungen wird es zunehmend schwieriger, eine Deployment Role aufzubauen. Zwar wird mit Tools wie dem IAM Access Analyzer der Aufwand etwas einfacher, jedoch ist es oft ein mühsames Verfahren, alle nötigen IAM Permissions herauszufinden. Hierbei haben wir gemerkt, dass trotz aller Abstraktionen ein Verständnis darüber, was sich „unter der Haube“ befindet, vorhanden sein muss.

Das Bootstrapping weist ohne eigene Anpassung der CloudFormation Execution Role die Administrator-Rolle zu. Ein Least-Privilege-Deployment durchzuführen ist zum Zeitpunkt dieses Blog-Artikels noch eine offene Diskussion: Least privilege deployments. Auch zwei Bootstrappings innerhalb einer Region sind derzeit nicht ohne Weiteres vorgesehen aber auch eher zweitrangig, da AWS selber empfiehlt, getrennte Workloads in jeweils eigene Accounts zu deployen.

Terraform

Terraform ist ein Infrastructure as Code-Tool der Firma HashiCorp. Konfigurationsdateien werden in der von HashiCorp kreierten Sprache HashiCorp Configuration Language (HCL) erstellt. In Teilen hat es Ähnlichkeiten zu YAML- oder JSON-Templates, gepaart mit eigener Syntax.

Unabhängigkeit von Regionen oder Accounts

Die Software der Firma ist unabhängig von spezifischen Cloud-Anbietern. Dies hat unter anderem den Vorteil, dass innerhalb eines Stacks mehrere Provider angegeben werden können. Das ermöglicht es, mit einem Stack innerhalb von AWS in unterschiedliche Regionen oder auch Accounts zu deployen. Terraform ist natürlich nicht nur auf AWS spezialisiert und es können daher auch weitere Provider anderer Cloud- oder Produktanbieter in ein Stack integriert werden. Da dieser Artikel den Fokus auf AWS hat, gehen wir bei Terraform hauptsächlich auf den offiziellen AWS-Provider von HashiCorp ein.

State-Handhabung

Auch dieses Tool benötigt einen State, der die aktuellen, in AWS erstellten Ressourcen speichert. Meist wird hier auf ein remote backend zurückgegriffen, das das State-File enthält. Das ermöglicht es, mit anderen zu kollaborieren. Dieser remote state sperrt beispielsweise den State, solange ein Deployment läuft, sodass in dieser Zeit niemand anderes gleichzeitig deployen kann. Terraform bietet hier viele unterschiedliche Möglichkeiten für remote backends. Die prägnantesten sind das von HashiCorp eigene Cloud Offering (Terraform Cloud) oder im Bereich AWS wird im kleineren Umfang oft auf das S3 Backend zurückgegriffen. Das S3 Backend besteht aus einem S3-Bucket und einer DynamoDB-Tabelle. Es bringt den Vorteil, dass hier kein weiterer Dienst anderer Anbieter benötigt wird. Ein Vorteil der HashiCorp-eigenen Cloud-Lösung ist, dass ein besseres Permission- und Compliance-Management geboten wird.

Schnittstellen

Der offizielle Terraform-Provider bietet eine breite Abdeckung an AWS-APIs und Möglichkeiten. Durch die Unabhängigkeit von Terraform bietet es auch eine umfangreichere AWS-Organisations-Integration, wodurch mit Terraform beispielsweise auch weitere AWS-Accounts angelegt werden können. Dennoch gilt auch bei Terraform: Vor dem Aufbau einer Infrastruktur ist es ratsam, via Evaluation zu verifizieren, dass alle benötigten Schnittstellen vorhanden sind.

Maintenance

In der Vergangenheit hat es durch neue HCL-Versionen die eine oder andere Breaking Change gegeben. Seitdem Terraform jedoch auch über ein v1-Release verfügt, ist die API ziemlich stabil. Daher hält sich die Maintenance in Grenzen. Jedoch ist dies auch stark abhängig vom eingesetzten Provider. Der offizielle Provider zu AWS ist vergleichsweise stabil.

Erweiterbarkeit

Auch Terraform bietet ein eigenes Modulsystem. Eine VPC-Konfiguration besteht beispielsweise im offiziellen AWS-Provider aus reinen Low-Level-APIs, die einiges an Konfiguration erfordern. Um sich hier die Arbeit zu erleichtern, existiert ein AWS-VPC-Modul, das den eigenen Konfigurationsaufwand drastisch reduziert.

Fehlen gewisse APIs in einem Provider, gibt es von Terraform selbst die Möglichkeit, eigene Provider zu schreiben. Dank des Multi-Provider-Prinzips ist es dann auch möglich, neben anderen den eigenen Provider in einem Stack zu verwenden. Um einen eigenen Provider zu schreiben, sind Kenntnisse in der Sprache Go erforderlich.

Best Practices

Als gute Praxis hat sich ergeben, dass Terraform-Stacks so klein wie möglich gehalten werden. Hierdurch können Blockaden durch aus der Reihe fallende Änderungen vermieden werden. Es kann passieren, dass wir in einem Bereich der Infrastruktur Änderungen vornehmen möchten, diese jedoch von anderen Änderungen im gleichen Terraform Stack blockiert werden. Beispiel: Eine relationale Datenbank hat ein Update durchgeführt und nun muss der remote state erneut mit den Ressourcen in der Cloud synchronisiert werden. Jedoch möchten wir Änderungen am Compute Layer vornehmen, der sich aber im gleichen Terraform-Stack befindet. Dies kann erst durchgeführt werden, wenn der backend state erneut synchronisiert wurde.

Ein kleiner Stack bedeutet nun einen Mehraufwand in der Projektkonfiguration von Terraform. Um sich auch hier nicht wiederholen zu müssen, werden oft weitere Hilfstools wie Terragrunt verwendet. Hierzu bietet Terragrunt ein eigenes Beispiel, das einige der Best Practices enthält: Example infrastructure-live for Terragrunt

Um mit Terraform gut arbeiten zu können, ist die Gewöhnung an das eigene Konzept der HCL-Sprache erforderlich. Vorteilhaft ist hier der terraform fmt-Command, der das eigene Template formatiert und etwas lesbarer gestaltet.

Kompatibilität

Aufgrund der Unabhängigkeit von Terraform zu AWS ist eine Kompatibilität zu CloudFormation oder zum Cloud Development Kit nicht direkt vorhanden. Um auf Informationen anderer Ressourcen zuzugreifen, kann hier auf andere AWS-Services zurückgegriffen werden (beispielsweise der Parameter-Store im Systems-Manager oder Secrets-Manager). Die Kompatibilität zwischen Terraform-Stacks kann entweder über die bereits genannten AWS-Services genutzt werden oder es gibt die Möglichkeit ein anderes Terraform State-File zu referenzieren. Hierbei kann dieses als Data-Source angegeben und die relevanten Informationen abfragt werden.

Testing

Bei Terraform wird der Stack explorativ durch das Ausrollen einer Ressource getestet. Um die Verifikation der Ressourcen nicht manuell durchführen zu müssen, existiert von den Entwicklern hinter Terragrunt eine Go Library, die es vereinfacht, automatisierte Tests für den Infrastruktur-Code zu schreiben: Terratest.

Gemeinsamkeiten

Was die Tools eint, ist die Handhabung der Konfigurationen. Da es sich bei allen Werkzeugen um CLI-Tools handelt, lassen sie sich beliebig in Pipelines und anderen Automationen einbauen. Außerdem berufen sich die Tools alle auf eine zugrunde liegende AWS-CLI, was die lokale Konfiguration von Credential-Profilen für die AWS-Zugriffe denkbar einfach macht.

Jedes der Tools hat zudem ein ähnliches Verhalten bei der Benutzung. Aktionen wie create, diff und deploy gehören bei allen zum Repertoire. Jedoch unterscheidet sich das Wording der Aktionen von Tool zu Tool. Dahinter steckt das Vorgehen des Ausrollens:

  1. create / install / init: Ich erstelle mir einen Stack, also einen Ausrollplan.
  2. ChangeSet / diff / plan : Ich vergleiche den Plan mit dem aktuellen Zustand und lasse mit die Änderungen auflisten.
  3. deploy / apply: Ich lasse die Änderungen ausführen und
  4. delete / destroy : lösche den Stack und alle enthaltenen Ressourcen.

Auch die Testbarkeit ist abgesehen von den Snapshot-Tests im Cloud Development Kit ähnlich. Es kann keine lokale Simulation des Deployments ausgeführt werden. Getestet wird immer direkt gegen AWS durch Ausprobieren der definierten Stacks. Daher hat das Testen immer einen explorativen Charakter und ist je nach AWS-Service mit Kosten verbunden.

Vorgehen

Der Development Workflow auf Nutzerseite ist vom Vorgehen her sehr ähnlich. Schauen wir uns das Ganze einmal aus einer übergeordneten Perspektive an:

Stack erstellen

  1. Wir definieren Infrastruktur via Config oder Code.
  2. Wir stoßen ein Deployment mit dem Template und der Angabe eines Stack-Namens an.
  3. Das Tool stellt fest, dass dieser Stack noch nicht existiert.
  4. Das Tool erstellt eine Abhängigkeitsstruktur, in der es Ressourcen erstellen muss.
  5. Das Tool deployt jede einzelne Ressource und speichert die erhaltenen Metadaten mit Konfiguration der Ressourcen im State.
  6. Sind alle Ressourcen deployt, ist der Stack erfolgreich ausgerollt. Tritt jedoch ein Fehler auf, wird das Deployment abgebrochen mit der Fehlermeldung, dass der Service abgebrochen wurde. Alle bisher erstellen Ressourcen werden wieder abgebaut.

Stack updaten

  1. Wir nehmen eine Anpassung oder Erweiterung der Ressourcen in unserem Projekt vor.
  2. Wir starten das Deployment erneut.
  3. Das Tool findet einen vorhandenen Stack.
  4. Das Tool vergleicht die beiden Konfigurationen und ermittelt ein Differenzial.
  5. Alle benötigten Anpassungen werden nacheinander durchgeführt. Es kann zu einer Neuerstellung der Ressource kommen, sofern ein Parameter angepasst wurde, den der spezifische Service nicht anpassen lässt. Vorteilhaft ist hier, vor dem Starten immer den diff zu untersuchen.
  6. Sind alle Änderungen erfolgreich vollzogen, ist das Stack-Update erfolgreich beendet. Tritt jedoch ein Fehler auf, wird ein Rollback der aktuell vorgenommenen Änderungen eingeleitet. Der Stack wird auf den vorherigen Stand zurückgesetzt. Sollte auch dieser Rollback in einen Fehler laufen, wird eine Fehlermeldung ausgegeben.

Unterschiede

Lernkurve

Wenn wir die Lernkurven der einzelnen Tools betrachten, ist in der Regel das erste immer das schwerste. Man muss sich zunächst mit der generellen Funktionsweise von IaC-Tools vertraut machen. Ein zweites oder drittes Tool ist dann weitaus schneller erlernbar. Da der IaC-Kontext dann bereits klar ist, müssen nur die Feinheiten des neuen Tools erlernt werden. Ein solcher Detail-Unterschied könnte beispielsweise die Arbeit mit Custom Resources sein.

Da es sich beim Cloud Development Kit um eine Abstraktion auf CloudFormation handelt, ist es hilfreich, CloudFormation zu verstehen. Dies erleichtert die Fehlersuche und ist sehr relevant beim Testen.

Regionenübergreifende Abhängigkeiten

CloudFormation bietet lediglich eine einfache Low-Level-API der Custom Resources. Um hier eine eigene Lösung zu integrieren, benötigt es oft auf der Nutzerseite zusätzlichen Aufwand. Das Cloud Development Kit ist hierbei umfangreicherer und erleichtert die Arbeit mit Custom Resources massiv. Bei Terraform entsteht das Problem gar nicht erst, da es auf dem Multi-Provider-Prinzip basiert. Hier ist rein die Programmiersprache Go erforderlich, um zusätzliche Custom Provider verfassen zu können.

State-Handhabung

Bei CloudFormation und Cloud Development Kit ist das State Handling vom Service klar definiert und für den Nutzer verborgen. Im Gegensatz dazu muss man sich bei Terraform aktiv darum kümmern und einlesen.

Syntax-Vergleich

Die Lesbarkeit des Codes hängt in jedem Fall stark von der Größe und Komplexität des Stacks ab. Dennoch sind auch hier Unterschiede der einzelnen Tools vorhanden.

Die folgenden Beispiele zeigen einen Codeausschnitt des jeweiligen Tools, das einen S3 Bucket erstellt. Im Key Management Service (KMS) wird ein Encryption Key angelegt und vom S3 Bucket für die Verschlüsselung der Daten verwendet. Es handelt sich hierbei um Code Snippets und keine fertigen Templates, die deployt werden können.

CloudFormation

Unter CloudFormation finden wir einen Stack in einem YAML-File wieder:

1KmsKey:
2  Type: AWS::KMS::Key
3  Properties:
4    KeyPolicy:
5      Version: 2012-10-17
6      Id: MyKeyPolicyId
7      Statement:
8        - Sid: Enable IAM User Permissions
9          Effect: Allow
10          Principal:
11            AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
12          Action: "kms:*"
13          Resource: "*"
14
15KeyAlias:
16  Type: AWS::KMS::Alias
17  Properties:
18    AliasName: alias/myKmsKey
19    TargetKeyId:
20      Ref: KmsKey
21
22Bucket:
23  Type: AWS::S3::Bucket
24  DeletionPolicy: Retain
25  Properties:
26    BucketEncryption:
27      ServerSideEncryptionConfiguration:
28        - ServerSideEncryptionByDefault:
29            KMSMasterKeyID: !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:${KeyAlias}"
30            SSEAlgorithm: "aws:kms"
31
32# Docs:
33# AWS S3 Bucket: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html
34# AWS KMS Key: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kms-key.html
35# AWS KMS Alias: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kms-alias.html

Dank YAML oder JSON ist zumindest die grundlegende Syntax meist bereits bekannt. CloudFormation bringt darüber hinaus eigene Keywords wie !Ref oder !Sub ein, um unter anderem Werte aus festen und variablen Anteilen zusammenbauen zu können.

In den beiden nächsten Tools hingegen werden die Referenzen besser ersichtlich.

Cloud Development Kit

Das Cloud Development Kit ist für Entwickler intuitiver, da man sich in einer Programmiersprache befindet, die bereits aus dem Arbeitsalltag bekannt ist. Es kann auf gängige Programmierbausteine zurückgegriffen werden. Beispielsweise sind Ressourcen referenzierbar und Abhängigkeiten klar verknüpft und nachvollziehbar:

1const myKmsKey = new kms.Key(this, 'MyKey');
2
3const bucket = new s3.Bucket(this, 'MyEncryptedBucket', {
4  encryption: s3.BucketEncryption.KMS,
5  encryptionKey: myKmsKey,
6});
7
8// Docs: https://docs.aws.amazon.com/cdk/api/v1/docs/aws-s3-readme.html

Der Code wird beim Deployment zu einem CloudFormation-Template synthetisiert. Somit ist das Cloud Development Kit mehr oder weniger ein Template-Generator. Dieses Vorgehen hat natürlich auch Auswirkungen auf die Art und Weise, wie wir Code schreiben. Bedeutet, ein Refactoring muss klar überlegt sein, da sich potenziell Ressourcen-Identifier ändern und beim Deployment diese Ressourcen mit alten IDs gelöscht und mit der gleichen Konfiguration neu erzeugt werden können.

Terraform

Terraform bringt, wie oben erwähnt, mit HCL (HashiCorp Configuration Language) eine eigene Sprache ins Spiel. HCL hat Ähnlichkeiten zu JSON und YAML mit eigenen Keywords und der Möglichkeit Variablen, Schleifen und Funktionen zu verwenden:

1resource "aws_kms_key" "mykey" {
2  description             = "This key is used to encrypt bucket objects"
3  deletion_window_in_days = 10
4}
5
6resource "aws_s3_bucket" "mybucket" {
7  bucket = "mybucket"
8
9  server_side_encryption_configuration {
10    rule {
11      apply_server_side_encryption_by_default {
12        kms_master_key_id = aws_kms_key.mykey.arn
13        sse_algorithm     = "aws:kms"
14      }
15    }
16  }
17}
18
19# Docs: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket#enable-default-server-side-encryption

Die HCL Template Syntax ist schnell erlernbar und auch gut lesbar. Ist die Vertrautheit geschaffen, fällt es einfach, sie wie YAML oder JSON zu lesen und verfassen. Nur bei Spezialfällen, die seltener vorkommen, ist das ein oder andere Mal die Dokumentation nötig. Leider gibt es hier kein Intellisense oder Vorschläge der IDE zu möglichen Werten der Provider. Aufgrunddessen ist hier oft die Dokumentation des Providers erforderlich.

Anwendungsfälle

Für IaC-Tools gibt es unzählig viele Anwendungsfälle. Auf alle können wir in diesem Artikel nicht eingehen. Dennoch wollen wir dir ein paar Beispiele nicht vorenthalten. Um die einzelnen Tools in Aktion zu sehen, haben wir für jedes Tool ein Beispiel mitgebracht.

IAM-Berechtigungen mit CloudFormation

Bei diesem Beispiel handelt es sich um IAM-Berechtigungen. Wir wollen eine Rolle mit Policy und einen IAM User anlegen.

Hierzu werden von CloudFormation direkt passende APIs angeboten. Wir können somit alles zusammen recht unkompliziert in einem Config-File definieren:

1---
2AWSTemplateFormatVersion: "2010-09-09"
3Description: IAM Anwendungsfall
4
5Resources:
6  IamUser:
7    Type: AWS::IAM::User
8    Properties:
9      ManagedPolicyArns:
10        - arn:aws:iam::aws:policy/AdministratorAccess
11      # ...
12
13  MyPolicy:
14    Type: AWS::IAM::ManagedPolicy
15    Properties:
16      PolicyDocument:
17        Version: '2012-10-17'
18        Statement:
19          - Sid: AllowStatement
20            Effect: Allow
21            Action:
22              - iam:ListUsers
23            Resource: "*"
24          # ...
25
26  RootRole:
27    Type: 'AWS::IAM::Role'
28    Properties:
29      AssumeRolePolicyDocument:
30        Version: '2012-10-17'
31        Statement:
32          - Effect: Allow
33            Principal:
34              Service:
35                - ec2.amazonaws.com
36            Action:
37              - 'sts:AssumeRole'
38          # ...
39      Path: /
40      ManagedPolicyArns:
41        - !Ref MyPolicy # hier wird die zuvor definierte Policy mit der Role verbunden

Bei IAM-Berechtigungen handelt es sich meist um einzelne Allow- oder Deny-Statements. Diese werden überwiegend über Namens-Patterns oder Tags zugewiesen oder eingeschränkt. Wir hatten es hier eher weniger mit direkten Referenzen zwischen Ressourcen zu tun. Hierdurch kann das Dokument eine gewisse Länge erreichen und dennoch ist es noch lesbar.

Dank der Stabilität der CloudFormation Template Syntax und APIs können wir mit hoher Wahrscheinlichkeit das Template lange Zeit liegen lassen, ohne dass wir Updates durchführen müssen. Dies ist sehr vorteilhaft, wenn wir es mit Ressourcen zu tun haben, die wir einmalig aufbauen und lange Zeit nicht oder nur selten anfassen wollen.

Backend Service mit dem Cloud Development Kit

Als zweites Beispiel haben wir einen Backend Service mitgebracht. Diesen wollen wir mithilfe des Cloud Development Kits aufbauen.

Der Backend Service enthält eine HTTP-API und einen Compute Layer, der in eine SQL-Datenbank schreibt. Die Datenbank und Compute-Ressourcen befinden sich in einem VPC.

Wir erstellen zu allererst unseren Stack. Dies ist eine Hülle, in die wir alle kommenden Code-Fragmente einsetzen können.

1import { join } from "path";
2import * as constructs from "constructs";
3import * as cdk from "aws-cdk-lib";
4import * as ec2 from 'aws-cdk-lib/aws-ec2';
5import * as rds from 'aws-cdk-lib/aws-rds';
6import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
7import * as apigateway from '@aws-cdk/aws-apigatewayv2-integrations-alpha';
8
9export class ManageWorkspaceUseCasesStack extends cdk.Stack {
10  constructor(scope: constructs.Construct, id: string, props?: cdk.StackProps) {
11    super(scope, id, props);
12
13    // hier werden wir alle weiteren Code Fragmente einsetzen...
14  }
15}

Als erste Ressource erstellen wir die VPC. Eine VPC in AWS enthält einiges an Komponenten (Internet Gateway, Subnets, Route Tables, etc.). Dank der Modularisierung des Cloud Development Kits können wir ein Modul verwenden, das uns die gesamte Komplexität abnimmt und alle erforderlichen Parameter mit passenden Defaults vorbelegt:

1const vpc = new ec2.Vpc(this, "VPC");
2
3const securityGroupDB = new SecurityGroup(this, "PostgresSecurityGroup", {
4  vpc: props.vpc,
5});
6securityGroupDB.addIngressRule(securityGroupDB, Port.tcp(5432));
7
8const securityGroupLambda = new SecurityGroup(this, "LambdaSecurityGroup", {
9  vpc,
10});
11securityGroupDB.addIngressRule(
12  securityGroupLambda,
13  Port.tcp(5432),
14  "Allow access from Lambda.",
15  true
16);

In diesem VPC können wir nun die Datenbank erstellen. Damit wir die Datenbank mit Serverless-Komponenten ansprechen können, nutzen wir einen RDS-Proxy, der für uns das Connection-Pooling übernimmt.

1const postgresSecret = new Secret(this, "DBSecret", {
2  generateSecretString: {
3    secretStringTemplate: JSON.stringify({
4      username: "master",
5    }),
6    generateStringKey: "password",
7    excludePunctuation: true,
8    includeSpace: false,
9  },
10});
11
12const cluster = new DatabaseInstance(this, "PostgresInstance", {
13  engine: DatabaseInstanceEngine.postgres({
14    version: PostgresEngineVersion.VER_11_5,
15  }),
16  credentials: Credentials.fromSecret(postgresSecret),
17  vpc,
18  vpcSubnets: {
19    subnetGroupName: "Postgres",
20  },
21  securityGroups: [securityGroupDB],
22  instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.MICRO),
23  allowMajorVersionUpgrade: false,
24});
25
26const proxy = cluster.addProxy("PostgresProxy-test", {
27  secrets: [postgresSecret],
28  vpc,
29  debugLogging: true,
30  borrowTimeout: Duration.seconds(30),
31  securityGroups: [securityGroupDB],
32});

Die Funktionalität des Services stellen wir über einzelne Lambda-Funktionen bereit. Hierzu bietet das Cloud Development Kit eine Integration, womit es neben der reinen Cloud-Ressource auch in der Lage ist, den Code mit auszurollen. Dies ist extrem praktisch, da wir nun nur noch ein Deployment-Skript haben, das für uns sowohl Infrastruktur als auch Code ausrollt. Hierzu nutzen wir das aws-lamda-nodejs-Modul. Beim Generieren des CloudFormation-Templates verpackt es unseren Code und rollt dieses Bundle später im Deployment automatisch mit aus.

1const lambda = new nodejs.NodejsFunction(this, 'Lambda', {
2  runtime: lambda.Runtime.NODEJS_14_X,
3  entry: join(__dirname, "lambda/handler.ts"),
4  handler: "handler",
5  timeout: cdk.Duration.seconds(4),
6  bundling: {
7    minify: true,
8    sourceMap: true,
9    target: "es2020",
10  },
11  environment: {
12    POSTGRES_SECRET_ARN: postgresSecret.secretArn,
13    PGHOST: proxy.endpoint,
14    PGSSLMODE: 'verify-ca',
15    PGDATABASE: 'postgres',
16  },
17  logRetention: logs.RetentionDays.THREE_DAYS,
18  tracing: Tracing.ACTIVE,
19  vpc,
20  vpcSubnets: {
21    subnetGroupName: 'Postgres',
22  },
23  securityGroups: [securityGroupLambda],
24});

Damit die Lambda-Funktion Zugriff auf die Datenbank bekommt, müssen wir diese hierzu berechtigen. An dieser Stelle hilft uns das Cloud Development Kit, dank der grant-API. Wir müssen hierzu nur im Code das Lambda berechtigen und das Cloud Development Kit generiert für uns die notwendige Berechtigung.

1postgresSecret.grantReadWriteData(lambda);

Zu guter Letzt wollen wir die Funktion über ein API Gateway erreichbar machen. Auch hierzu können wir ein Modul nutzen, das uns einiges an Arbeit abnimmt.

1const lambdaIntegration = new apigateway.HttpLambdaIntegration(
2  "Integration",
3  lambda
4);
5
6const httpApi = new apigwv2.HttpApi(this, "HttpApi");
7httpApi.addRoutes({
8  path: "/",
9  methods: [apigwv2.HttpMethod.ANY],
10  integration: lambdaIntegration,
11});

Netzwerksetup mit Terraform

Als dritten Beispiel haben wir ein Netzwerksetup mitgebracht.

Hierbei haben wir eine Domain bei unserem Registrar liegen. Diese wollen wir nun mit einem DNS-Server in einem unserer Accounts verknüpfen. Des Weiteren haben wir in einem anderen Account einen weiteren DNS-Server für eine Subdomain. Die beiden DNS-Server möchten wir via Zone Delegation verbinden, sodass ich bei einer Anfrage an die Subdomain über den root-DNS-Server an den DNS-Server der Subdomain weitergeleitet werde.

Bei diesem Use Case haben wir neben den beiden DNS-Servern, die es aufzubauen gilt, auch einen Account-übergreifende Abhängigkeit. Wir müssen die DNS-Server-Adressen des einen Servers als NS Records im Root-Server hinterlegen. Hierbei kommt uns das Multi-Provider-Prinzip von Terraform zugute. Wir können neben mehreren Providern auch einfach den AWS-Provider zweimal in einem Projekt verwenden, um den Zugang zu beiden Accounts zu legen.

Definieren wir also zuerst die beiden Provider für die jeweiligen Accounts:

1variable "network_profile" {
2  type        = string
3  description = "network profile"
4}
5variable "stage_profile" {
6  type        = string
7  description = "staging profile"
8}
9
10provider "aws" {
11  alias   = "network"
12  region  = "eu-central-1"
13  profile = var.network_profile
14}
15
16provider "aws" {
17  alias   = "stage"
18  region  = "eu-central-1"
19  profile = var.stage_profile
20}

Sobald die Provider vorhanden und erreichbar sind, können wir nun die beiden DNS-Server in den jeweiligen Accounts definieren. Über den provider-Key können wir angeben, in welchem AWS-Account die Ressourcen aufgebaut werden soll.

1variable "domain_name" {
2  type        = string
3  description = "TLD domain name"
4  default     = "acme.app"
5}
6resource "aws_route53_zone" "main" {
7  provider = aws.network
8  name     = var.domain_name
9
10  tags = {
11    Name        = "root-dns"
12    Environment = "production"
13    ManagedBy   = "terraform"
14    Role        = "public"
15  }
16}
17
18variable "subdomain_name" {
19  type        = string
20  description = "TLD domain name"
21  default     = "stage.acme.app"
22}
23resource "aws_route53_zone" "sub" {
24  provider = aws.stage
25  name     = var.subdomain_name
26
27  tags = {
28    Name        = "sub-dns"
29    Environment = "stage"
30    ManagedBy   = "terraform"
31    Role        = "public"
32  }
33}

Nachdem beide DNS-Server aufgebaut sind, können wir nun die Abhängigkeit zwischen den beiden über die Zone Delegation herstellen:

1# Zone Delegation Setup
2data "aws_route53_zone" "main" {
3  provider = aws.network
4  name     = var.domain_name
5}
6resource "aws_route53_record" "cc-ns" {
7  provider = aws.network
8  zone_id  = data.aws_route53_zone.main.zone_id
9  name     = var.subdomain_name
10  type     = "NS"
11  ttl      = "30"
12  records  = aws_route53_zone.sub.name_servers
13}

Fazit

Alle Tools, die wir uns angeschaut haben, besitzen einen hinreichenden Umfang an Funktionalität sowie Möglichkeiten der Erweiterbarkeit, sodass diese guten Gewissens eingesetzt werden können. Wir haben aber auch gesehen, dass es hier keinen klaren Sieger gibt. Basierend auf ihren Eigenschaften sind gewisse Use Cases einfacher mit dem einen Tool, andere mit einem anderen Tool umzusetzen.

Wir haben gelernt, dass die Wahl eines passenden Tools von den Charakteristiken des Tools, dem Use-Case sowie dem Kontext, in dem wir uns befinden, abhängig ist.

Um bei der Entscheidung für ein passendes Tools zu helfen, haben wir eine kleine Entscheidungshilfe gebaut: Terraform eignet sich besonders gut im Fall von Cross-Region- und Cross-Account-Stacks, da durch die verschiedenen Provider diese alle in einer Codebase miteinander referenziert werden können. Hat man es mit sehr viel Struktur zu tun und möchte gegebenenfalls sogar Lambda-Code gleich mit im Stack verarbeiten, ist CDK unsere Empfehlung. Ist es jedoch eine sehr übersichtliche, simple Struktur, die auch recht selten in Zukunft verändert wird, profitiert man sehr von der Einfachheit und Stabilität von CloudFormation.

Wir hoffen, wir konnten euch einen Überblick über Teile der aktuellen Infrastructure-as-Code-Landschaft geben.

Viel Spaß bei euren zukünftigen Infrastructure-Automationen!

Beitrag teilen

Gefällt mir

7

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.