Wir starten die Serie zum Thema Infrastructure as Code (IaC) mit Terraform mit einer Einführung in allgemeine IaC-Aspekte und erfahren, welche IaC-Ansätze für Microsoft Azure verfügbar sind. Wir richten Terraform lokal ein und führen ein erstes Beispiel-Deployment durch. Im weiteren Verlauf der Serie wird es um Dynamisierung des Codes, DevOps Pipelines für IaC und Möglichkeiten der Testautomatisierung gehen.
Ressourcen in der Azure Cloud oder in einem anderen Hyperscaler können auf unterschiedlichste Art und Weise erstellt, konfiguriert und wieder gelöscht werden. Ein einfacher und bequemer Weg ist zum Beispiel ein GUI wie das Azure Portal. Es ist übersichtlich und unterstützt das Deployment oder Änderungen häufig mit Wizards. In Azure ist es außerdem möglich, mit dem nativen Kommandozeilentool Azure CLI zu arbeiten oder Module von PowerShell zu verwenden. Des Weiteren stehen diverse SDKs z. B. für C#, Java oder Python zur Verfügung. Am Ende landet – bezogen auf Microsoft – alles beim Azure Ressource Manager, dem zentralen API (Abb. 1).
Abb. 1: Azure Resource Manager API [1]
Die Verwendung von GUIs in wichtigen produktiven Umgebungen ist allerdings nur bedingt empfehlenswert, da dieser Weg einige Nachteile mit sich bringt. Das Erstellen mag einfach funktionieren, doch später tauchen Fragen auf: Wie wurde die Ressource erstellt und von wem? Ist es noch die ursprüngliche Konfiguration oder wurde sie geändert? Und ist sie valide und stimmt mit gesetzten Standards überein? Wie lange dauert das Durchlaufen des Wizards im Portal? Was ist, wenn ein Deployment wiederholt werden soll? Ressourcen könnten zudem versehentlich gelöscht oder fehlkonfiguriert werden.
Die Azure-Logs können bei der Spurensuche helfen und mit Policys können Security- und Governance-Standards gesichert werden. Gelöst wird damit aber nur ein Teil der Probleme. Zudem sollen Ressourcen, wie in der Enterprise-IT üblich, in der gleichen Weise auf verschiedene Stages deployt werden, beispielsweise auf Entwicklung, Test und Produktion. Das ist manuell sehr zeitaufwendig, fehleranfällig und kann zu eigenständigen Insellösungen bzw. „Schneeflocken“ führen, wenn die Umgebungen nicht mehr konsistent sind.
Um eine stabile Cloud-Umgebung zu erhalten, führt daher kein Weg an IaC vorbei. Statt die Ressourcen im Portal mit vielen Klicks zu erstellen und später nicht mehr zu wissen wie, wird Code geschrieben, in einem Repository abgelegt und in der Regel von dort aus provisioniert.
Die Vorteile liegen auf der Hand und werden Entwickler:innen bekannt sein: Es ist nachvollziehbar, was wann gemacht wurde, es gibt Versionierung. Teamarbeit ist möglich, und mit der entsprechenden Parametrisierung kann die Umgebung problemlos auf verschiedene Stages ausgerollt werden. Deployments können automatisiert und einfach wiederholt werden. Zur Vermeidung der angesprochenen Nachteile gibt es daher in Projekten häufig auch die Vorgabe, Cloud-Ressourcen nur per IaC zu deployen. Das Portal soll in dem Fall meistens nur für bestimmte User zum Troubleshooting zur Verfügung stehen. Mit DevOps und Pull-Request-Strategien können Standards für die Konfiguration zusätzlich unterstützt werden. Eine Ressourcenbestellung innerhalb des Unternehmens, z. B. einer virtuellen Maschine, ist keine langwierige Angelegenheit mehr, sondern kann mit einem vorlagenbasierten Prozess automatisiert werden. Die Entwicklung des Codes kostet zwar Zeit, Fehlersuche und manuelle Vorgänge aber in der Regel noch mehr. IaC gibt Sicherheit und Reproduzierbarkeit – z. B. mit Terraform [2].
Zur Abrundung der grundsätzlichen Beschreibung von IaC seien noch ein paar Begriffe erklärt, die in dem Kontext oft verwendet werden:
Push vs. Pull
Idempotence
deklarativ vs. imperativ
Der erste Punkt beschreibt die Art und Weise, wie die Konfiguration der Infrastruktur übertragen wird: aktiv durch Kommandos (push) – hier ist Terraform einzuordnen – oder passiv (pull), wobei die Konfiguration an einer zentralen Stelle abgelegt und dann von einer Art Agent abgeholt wird.
Idempotence bedeutet, dass ein konsistenter Zustand des Zielsystems erhalten wird und der Konfiguration im Code entspricht, egal wie häufig ein Deployment ausgeführt wird: Wenn sich im Code nichts verändert hat, ändert sich am Deployment-Ziel auch nichts.
Außerdem verfolgt Terraform den [deklarativen Ansatz]: Statt Schritt für Schritt per Befehl genau vorzuschreiben, was getan werden soll (imperativ), wird hierbei lediglich der gewünschte Endzustand beschrieben. Die Differenz zwischen dem Ist- und dem Sollzustand wird vom Tooling ermittelt und die entsprechenden Änderungen werden durchgeführt. Die genaue Abfolge der Befehle kümmert den Anwender im Detail dann nicht mehr [3].
Der Schwerpunkt dieser Serie, Terraform, ist nur eine Option für die Verwendung von IaC. Während Terraform einen Cloud-Anbieter-übergreifenden Ansatz verfolgt, bieten Hyperscaler native Optionen. Im Bereich der Azure Cloud sind das in der ursprünglichen Form die Azure-Resource-Manager-(ARM-)Templates. Dabei handelt es sich um JSON-basierte Dateien, in der die zu erstellenden Ressourcen und Parameter definiert werden. Diese Dateien können dann geparst und der Inhalt durch den Resource Manager verarbeitet werden. Von jeder erstellten Ressource kann ein solches Template über das Azure Portal exportiert werden. Die Anzahl der Codezeilen ist im Vergleich zu anderen Optionen deutlich höher, und das korrekte Definieren von Abhängigkeiten kann kompliziert werden. Obwohl sie als nicht mehr zeitgemäß für einen komplexen IaC-Ansatz gelten, sind sie keineswegs wegzudenken. Unter der Haube finden sie häufig noch Verwendung, etwa in Azure Policys oder bei Services wie der Azure Data Factory, wo Konfigurationsänderungen durch generierte ARM-Templates verarbeitet werden. Der modernere Azure-eigene Ansatz ist Bicep. Dabei handelt es sich um einen Wrapper, der das Azure-native Entwickeln von IaC-Skripten deutlich vereinfacht. Die Sprache bietet viele hilfreiche Funktionen und vereinfacht das Schreiben von Abhängigkeiten zwischen den Ressourcen sehr bzw. kann diese selbstständig behandeln. Am Ende werden für das Deployment wiederum ARM-Templates generiert, über die dann mit dem API kommuniziert wird [4]. In der Praxis werden diese drei Optionen im gesamten Kontext eines Deployments, beispielsweise einer Azure DevOps Pipeline, häufig mit PowerShell oder einem Azure-CLI-Task kombiniert. Nicht jede gewünschte Einstellung ist mit Terraform oder Bicep möglich bzw. praktisch gut umsetzbar.
Terraform stammt von der Firma HashiCorp und bietet Anwender:innen die Möglichkeit, IaC für eine Vielzahl von Providern umzusetzen. Viele gängige Provider sind als Plug-ins bereits vorhanden. Dazu zählen neben der Azure-Cloud, Amazon AWS oder Google Cloud unter anderem auch Docker und Kubernetes. Insgesamt gibt es mittlerweile über 1 000 solcher Plug-ins. Ist der benötigte Provider nicht darunter, kann die Liste durch ein eigenes Plug-in erweitert werden. Terraform arbeitet mit der HashiCorp Configuration Language (HCL), welche die grundsätzliche Struktur definiert und mit Expressions sowie Templatefunktionen dynamisiert werden kann. Die genaue Spezifikation findet sich Open Source auf GitHub [5], ebenso der azurerm-Provider [6] für die Ressourcen der Microsoft-Cloud. Terraform liegt aktuell in Version 1.3.2 vor und ist mittlerweile sehr stabil geworden.
Um Terraform verwenden zu können, muss lediglich das Binary Package heruntergeladen werden. Bei der manuellen Installation sollte auch die PATH-Variable [7] gesetzt werden, damit die Laufzeitumgebung für die Terraform-Befehle gefunden wird. Je nach Betriebssystem stehen für den Download neben der manuellen Option auch Package Manager wie Homebrew oder Chocolatey zur Verfügung [8].
An sich könnte nun direkt gestartet werden, die integrierten Konsolen wären für das Absenden der Befehle ausreichend. In der Regel wird aber natürlich auf eine komfortablere Entwicklungsumgebung gesetzt. Sehr gut geeignet ist hier Visual Studio Code (VS Code): Das Tool ist kostenlos, leichtgewichtig und Open Source. Es verfügt über eine praktische Git-Integration und kann mit zahlreichen sinnvollen Extensions erweitert werden, passend für den jeweiligen Anwendungsfall. Für die Arbeit mit Terraform lohnt sich der Blick auf folgende Erweiterungen:
Azure Terraform [10]
Go (für die IaC-Testautomatisierung mit Terragrunt)
JSON Tools (Eric Lynd)
Damit werden Entwickler:innen durch Features wie Syntaxhervorhebung, IntelliSense (STRG + SPACE), Navigation im Code und automatische Formatierung unterstützt. Die nötigen providerspezifischen Module müssen im Vorfeld nicht installiert werden, das geschieht automatisch während des Deployment-Prozesses.
Das Prinzip und die Schritte eines Terraform Deployments sollen an einem einfachen Beispiel verdeutlicht werden. Hierfür wird ein Storage-Account provisioniert. Zum Start brauchen wir lediglich eine einzelne Datei, nach Best Practice benannt main.tf, mit dem Inhalt von Listing 1.
Listing 1
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "sto_rg" {
name = "rg-storage"
location = "West Europe"
}
resource "azurerm_storage_account" "mystorage" {
name = "uniquestorage08154711"
resource_group_name = azurerm_resource_group.sto_rg.name
location = azurerm_resource_group.sto_rg.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = {
cost-center = "4461"
}
}
Damit wird ein neuer Storage-Account mit minimaler Konfiguration in einer neuen Ressourcengruppe erstellt. Größere Anwendungen weisen dann üblicherweise eine Ordnerstruktur und verschiedene Dateien auf. Die Provider-Sektion mit dem features-Block darf hier nicht fehlen, auch wenn sie in dieser Form nur mit Standardwerten arbeitet.
Bei Terraform kommen verschiedene Dateiendungen zur Anwendung, meistens finden sich:
.tf (für die Konfiguration, d. h. den Code des IaC)
.tfvars (Inputfiles für einzulesende Variablen)
.tfstate (Verwaltung vom Terraform State)
.json (z. B. Schemainformationen)
Im Verlauf der Serie werden wir auf die Details noch eingehen.
Bei Terraform wird von Konfiguration gesprochen, aber es handelt sich hier nicht (nur) um Parameter oder Ähnliches, gemeint ist der gesamte IaC-Code inklusive zu erstellender Ressourcen oder Datenabrufe. In den Dateien ist in der Regel eine Vielzahl von Blöcken vorhanden. Ein Block folgt dem Aufbau Typ – Ressourcentyp – Referenzname. Innerhalb eines Blocks werden Einstellungen über Key-Value-Paare oder spezielle eingebettete Blöcke für Objekttypen definiert (Abb. 2).
Abb. 2: Einstellungen innerhalb eines Blocks
In unserem Beispiel haben wir Ressourcenblöcke mit den Typen für die Ressourcengruppe und den Storage aus dem Provider, ebenso die Namen, mit denen in der Konfiguration darauf zugegriffen werden kann. Für Deployments in der Microsoft-Cloud wird der azurerm-Provider verwendet. Als Typen für Values kennt Terraform Zeichenketten, Zahlen, Booleans, Arrays und Objekte:
string = "Hallo123"
number = 100
bool = false
list = ["Item1", "Item2"] # arrays
map = {costcenter = "123", owner = "cloudhero"} # objekte
Bevor wir das erste Deployment starten, wollen wir noch kurz den Blick auf die Referenzierung richten: Auch der Storage-Account braucht zwingend die Angabe von Region und Ressourcengruppe. Diese könnten auch als Zeichenketten angegeben werden. Unsere Referenzmethode im Code sorgt dafür, dass Tippfehler vermieden werden, da die Werte aus den Attributen der Ressourcengruppe gelesen werden. Auch sorgt diese Methode dafür, dass Abhängigkeiten der Ressourcen und die Reihenfolge im Deployment-Prozess richtig gesetzt werden – die Ressourcengruppe muss schließlich vor dem Storage erstellt werden. Gibt es Komplikationen bei den Abhängigkeiten, kann mit dem Attribut [depends on] die Reihenfolge für die Ressourcen forciert werden.
Bei einem Doppelklick auf eine Terraform-Datei passiert nichts bzw. es öffnet sich ein Editor. Um die gewünschten Änderungen oder Abfragen durchzuführen, wird das Terraform CLI verwendet. Mit einem beliebigen Kommandozeilentool können über den Befehl terraform als Interface die entsprechenden Anweisungen zur Verarbeitung durch Terraform abgesendet werden. Sehr hilfreich bei der Verwendung des CLI ist das -help-Kommando, das auf verschiedenen Ebenen funktioniert, z. B. direkt auf dem Root-Level oder spezifisch für ein Subkommando wie terraform init -help.
Relevant für die Ausführungen der Terraform-Konfiguration mit der Konsole ist der aktuelle Pfad, von dem die Kommandos abgesetzt werden, sowie alles, was sich darunter befindet. Sämtliche Ordner und Dateien werden automatisch miteinbezogen [11].
Ein zentraler Bestandteil von Terraform ist die Verwaltung des aktuellen Zustands (State) des Ressourcen-Deployments. Dieser wird standardmäßig in einer lokalen Datei mit dem Namen terraform.tfstate – genannt Statefile – abgelegt. Hier wird von einem lokalen Backend auf der Clientmaschine gesprochen. Häufig ist das Statefile aber remote konfiguriert, beispielsweise in einem Azure-Storage-Account oder einem Amazon S3 Bucket. Die Statefile-Datei ist im JSON-Format geschrieben und beinhaltet ein Mapping zwischen der geschriebenen IaC-Konfiguration und den Remoteobjekten, sprich den Ressourcen beim Provider. Wird eine Ressource erstellt, werden deren Identity und Konfiguration im State festgehalten, um auch für spätere Operationen eine eindeutige Zuordnung zu ermöglichen.
Terraform erfasst auch den Providerressourcentyp sowie den internen Namen. Durch das Aktualisieren des States vor Änderungsoperationen wird der nötige Soll-Ist-Abgleich vollzogen und lediglich die Veränderungen werden deployt.
Die essenzielle Bedeutung wird in einem einfachen Beispiel deutlich: Die in einem ersten Schritt erstellte Ressourcengruppe soll umbenannt werden. Die Konfiguration wird umgeschrieben, und mit Hilfe der getrackten Informationen im State ist es für Terraform möglich, die korrekte Gruppe beim Provider zu finden, sie am Ende zu löschen und eine neue zu erstellen, anstatt dass beide parallel vorhanden sind. Werden z. B. nur Taginformationen geändert, kann ein Update gemacht werden, ohne dass die Ressource gelöscht und neu erstellt wird.
Ein weiterer Grund für die Deployment-Verwaltung mit Hilfe des Statefiles ist das Erfassen von Abhängigkeiten zwischen Ressourcen für Löschvorgänge, beispielsweise für virtuelle Maschinen, und deren VNet/Subnet-Integrationen. In der Konfiguration sind diese erkennbar, aber wenn sie aus dem Code entfernt werden, kann sich Terraform des States bedienen, um die korrekte Reihenfolge beizubehalten. Zudem kann es bei sehr großen Deployments aus Performancegründen oder wegen Provider-API-Restriktionen sinnvoll sein, das Aktualisieren des States bei der Planerstellung zu übergehen. Stattdessen werden dann Cachewerte für die Ressourcenattribute im Statefile verwendet. Das Statefile sollte niemals in einem Repository abgelegt werden, da sensitive Informationen wie Secrets dort stets im Klartext zu sehen sind [12].
Von manuellen Änderungen im State wird dringend abgeraten; falls Anpassungen nötig sind, sollten diese mit den entsprechenden CLI-Kommandos wie terraform import vorgenommen werden [13], [14]. Abbildung 3 zeigt den Ausschnitt eines Statefile für die Ressourcengruppe.
Abb. 3: Ausschnitt eines Statefile für die Ressourcengruppe
Interne Namen der Ressourcen in der Konfiguration sollten nicht einfach geändert werden. Wird wie im Beispiel aus sto_rg etwa sto_rg123, gibt es einen Mappingfehler (Abb. 4). Ist das aber nötig, schafft das move-Kommando für das Statefile Abhilfe:
terraform state mv azurerm_resource_group.sto_rg azurerm_resource_group.sto_rg123
Ab der Version 1.1 geht das auch mit einem moved-Block in der Konfiguration [15].
Abb. 4: Mappingfehler bei Änderung des Ressourcennamens
Eine detaillierte Dokumentation dazu, wie die Providermodule angesprochen werden können, ist unter [16] zu finden. Nach einem einheitlichen Schema kann wie im Beispiel des Storage-Account mit Referenzen und Beispielen nachgelesen werden, welche Attribute mit entsprechender Syntax angegeben werden können bzw. müssen. Darüber hinaus beschreibt die Dokumentation jeweils, welche Datenabfragen für die Ressourcen möglich sind, und wie ein Import für das Statefile funktioniert [17].
Am Beispiel des Storage Deployments erklären wir die Hauptkommandos von Terraform:
terraform init
terraform fmt
terraform validate
terraform plan
terraform apply
terraform destroy
Der init-Befehl bereitet das Deployment vor. Das definierte Backend für das Statefile wird initialisiert und die nötigen Rechte darauf überprüft. Die Konfiguration wird durchsucht, und es werden, falls noch nicht vorhanden, die nötigen Plug-in-Module vom jeweiligen Provider der Ressourcen heruntergeladen. Die Informationen der gezogenen Provider landen dann in einer Binärdatei im .terraform-Ordner und in einer _.lock-_Datei, die von Terraform verwaltet wird (Abb. 5) [18].
Abb. 5: lock-Datei im Terraform-Ordner
Terraform fmt hilft bei der Lesbarkeit des Codes, indem es eine einheitliche Formatierung auf die Konfigurationsdateien anwendet und z. B. überflüssige Leerzeichen oder Tabs entfernt. validate kann angewendet werden, um „trocken“ die Syntax des Codes zu überprüfen. Mit diesem Schritt wird beispielsweise festgestellt, ob Referenzen in der Konfiguration korrekt sind, keine doppelten Ressourcen als Block definiert sind oder schließende Hochkommas oder Klammern fehlen. Das Provider-API wird allerdings nicht aufgerufen, d. h., Authentifizierungsfehler oder ein bereits vergebener Name werden so nicht entdeckt.
Der plan-Schritt sorgt dann dafür, dass der Inhalt des States aktualisiert wird, um den Zustand eventuell bereits vorhandener Ressourcen zu reflektieren (Abb. 6). Dieser wird dann mit der Konfiguration gemäß Code verglichen und, falls nötig, wird ein Set an Änderungen vorgeschlagen, um den gewünschten Zustand zu erreichen. Damit kann gut überprüft werden, ob Terraform die korrekten Änderungen vornehmen möchte – was soll hinzugefügt, was gelöscht oder welche Ressourcen sollen geändert werden (Abb. 7 und 8). Mit dem Kommandozusatz -out=File kann dieser Plan als lauffähiges Artefakt abgespeichert werden und später als Basis für das Deployment dienen. Das wird vor allem in automatisierten DevOps-Prozessen zur Vermeidung von Überraschungen verwendet; ansonsten wird von einem spekulativen Plan gesprochen, denn zwischen plan und apply könnten sich in der Zwischenzeit Änderungen an den Ressourcen ergeben haben. Dieser Schritt wird auch bei jedem apply-Kommando mit durchgeführt [19].
Abb. 6: Beispiel
Abb. 7: plan-Schritt 1
Abb. 8: plan-Schritt 2
Abb. 9: Ressourcendetails werden ausgewiesen
Mit terraform apply werden schließlich – nach Bestätigung oder direkt mit dem Zusatz -auto-approv – die geplanten Änderungen an die Provider-APIs übermittelt. Gibt es keine Konflikte bei den Ressourcenattributen oder Authentifizierungsprobleme, wird das erfolgreiche Deployment zusammengefasst und gewünschte Ressourcendetails als Outputwerte ausgewiesen (Abb. 9) [20]:
Apply complete! Resources: 1 addes, 1 changed, 1 destroyed.
Um die beschriebene Konfiguration beim Provider später wieder zu löschen, kann der Befehl terraform destroy verwendet werden (Abb. 10). Auch hier geht man über den Inhalt des States als Mapping für die zu entfernenden Ressourcen. Wird die nötige Bestätigung nicht übergangen, können die Details in einer Übersicht noch einmal überprüft werden. Destroy hat den gleichen Effekt, wie wenn sämtliche Ressourcen aus der Codekonfiguration gelöscht würden und anschließend apply angewendet würde [21].
Abb. 10: Der Befehl destroy
In diesem Beitrag haben wir einen Überblick über allgemeine IaC-Themen sowie die Funktions- und Arbeitsweise von Terraform mit den wichtigsten Kommandos gegeben, um den Einstieg in die komplexe Thematik zu erleichtern. Das nächste Mal werden wir eine Terraform-Konfiguration dynamisieren und in eine Datei/Ordnerstruktur überführen, wie es gängige Praxis ist.
Im zweiten Teil der Serie zu Terraform geht es um die diversen Optionen, wie sich der Code dynamisieren und flexibel gestalten lässt. Außerdem wird gezeigt, was es genau mit dem Statefile und den Providern auf sich hat.
[1] https://learn.microsoft.com/de-de/azure/azure-resource-manager/management/overview
[2] https://www.atlassian.com/de/microservices/cloud-computing/infrastructure-as-code
3] https://www.redhat.com/de/topics/automation/what-is-infrastructure-as-code-iac
[4] https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/overview?tabs=bicep
[5] https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md
[6] https://github.com/hashicorp/terraform-provider-azurerm
[7] https://stackoverflow.com/questions/1618280/where-can-i-set-path-to-make-exe-on-windows
[8] https://learn.hashicorp.com/tutorials/terraform/install-cli
[9] https://marketplace.visualstudio.com/items?itemName=HashiCorp.terraform
[10] https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azureterraform
[11] https://developer.hashicorp.com/terraform/cli/run
[12] https://blog.gruntwork.io/how-to-manage-terraform-state-28f5697e68fa
[13] https://developer.hashicorp.com/terraform/language/state/purpose
[14] https://developer.hashicorp.com/terraform/language/state
[15] developer.hashicorp.com/terraform/language/modules/develop/refactoring
[16] https://registry.terraform.io
[17] https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account
[18] https://developer.hashicorp.com/terraform/cli/init
[19] https://developer.hashicorp.com/terraform/cli/commands/plan
[20] https://developer.hashicorp.com/terraform/cli/commands/apply
[21] https://developer.hashicorp.com/terraform/cli/commands/destroy