kh24-tf-talk/Folien.md
2024-05-10 10:55:13 +02:00

28 KiB

type slideOptions
slide
transition theme
fade white

Infrastructure as Code mit Terraform/OpenTofu

Eine Schnelleinführung


Hi. 👋

  • Fediverse: @thunfisch@chaos.social
  • In der Lohnarbeit mache ich "Cloud" Infrastrukturdinge.
  • Ich nutze dafür seit einigen Jahren Terraform. Ich mag's.
  • Das hier ist eine Schnelleinführung zu Terraform.

Note:

Hi. Ich bin thunfisch, und für meine Lohnarbeit mach ich viele Infrastrukturdinge in der "Cloud". Wir betreiben ziemlich viel Infrastruktur, und um unser Leben einfacher zu machen, nutzen wir dafür seit einigen Jahren Terraform. Es hat durchaus ein paar Macken, aber im Großen und Ganzen mag ich's. Das hier wird ein Schnellüberblick, in einem sehr hohen Tempo. Ich lasse viele Feinheiten aus, aber es sollten die wichtigsten Aspekte um mit Terraform loszulegen enthalten sein. Mir ist es wichtig, dass ihr am Ende versteht was Terraform konzeptionell macht, und wie es intern funktioniert. Der Rest steht im Zweifel eh im Manual, dafür braucht es keinen Vortrag.


Folien

https://git.jankoppe.de/j/kh24-tf-talk

asdf


Was ist Infrastructure as Code?

Oft abgekürzt als "IaC"

Meine lockere Definition: Diverse Infrastruktur maschinenlesbar beschreiben, und durch ein Programm ausführbar machen, sodass die Beschreibung zur Realität wird.

Note:

Um zu verstehen für was Terraform benutzt wird, hilft es den Begriff "Infrastructure as Code" zu erklären. Meine lockere Definition: Diverse Infrastruktur machinenlesbar beschreiben, und durch ein Programm ausführbar machen, sodass die Beschreibung zur Realität wird. Terraform ist ein solches "Infrastructure as Code" tool. Über die perfekte Definition kann man sich sicherlich streiten. Gerade der Aspekt "Code" wird gerne kritisiert, weil man z.B. bei Terraform nicht direkt programmiert. Aber ich denke für heute ist das gut genug.


Warum Infrastructure as Code?

  • Reproduzierbarkeit
  • Wiederverwendbarkeit
  • Lesbarkeit
  • Vereinheitlichung

Note:

  • Reproduzierbarkeit bedeutet hier: Ich kann sicherstellen, dass ich bei Veränderungen den gewünschten Zustand wieder hergestellt bekomme.
  • Wiederverwendbarkeit - Im Sinne von: Ich kann die selbe Architektur immer und wieder produzieren - Aufwand des Setups nur einmal
  • Lesbarkeit - Die Beschreibung ist eine Beschreibung, wortwörtlich. Ich habe idealerweise alle Aspekte meiner Infrastrukturschicht in IaC niedergeschrieben. IaC ersetzt eine Dokumentation aber nicht, da gute Dokumentation nicht nur beschreiben, sondern auch erklären sollte.
  • Vereinheitlichung - Idealerweise kann euer IaC tool mit allen Infrastrukturen umgehen. Dann lernt ihr nur eine Sprache, die ihr immer wieder verwenden könnt - und Infrastrukturabhängigkeiten direkt an einer Stelle ausdrückt.

Diese Vorteile könnte man auch sehr gut auf Configuration Management Software übertragen.


Configuration Management

IaC != Configuration management

Note:

Kurzer Hinweis: Es gibt viele Tools aus dem Bereich "configuration management" und automatisierung, zum Beispiel Puppet, Chef, Ansible. Terraform hat nicht den Anspruch diese zu ersetzen. Es arbeitet auf einer anderen Ebene: Der Infrastruktur an sich, also vor allem die Dinge die sich außerhalb einer VM befinden. Die Tools aus dem Bereich configuration management und automatisierung fokussieren sich eher auf das, was innerhalb einer VM passiert.


Terraform

  • Eins der bekanntesten IaC tools
  • Initiiert und hauptsächlich entwickelt von HashiCorp
  • bis Version 1.5.x MPL-2.0 (OSS)
  • seit Version 1.6.0 BUSL 1.1 (Non-free)
  • deshalb OpenTofu Fork - MPL-2.0, Linux Foundation

Note:

Nun ein paar Randinfos zu Terraform an sich. Es ist mit eins der bekanntesten und universellsten IaC tools, und wurde vor circa 10 Jahren von HashiCorp gestartet, und ist auch weiterhin von ihnen geführt. Ende letzten Jahres hat HashiCorp für all ihre Produkte die Lizenz auf die nicht-freie Business Source License gewechselt, was in der gesamten Community auf wenig Gegenliebe stoß. Wie so üblich wurde fleißig geforkt, und es gibt nun mit OpenTofu einen Fork unter der Mozilla Public License 2.0, welcher von der Linux Foundation betreut wird.

Derzeit sind Terraform und OpenTofu noch kompatibel untereinander, wennauch OpenTofu featuremäßig etwas hinterherhinkt, da sie neue Features erneut implementieren müssen um Open Source zu bleiben. Ich würde aber seit dem Verkauf von HashiCorp an IBM eine starke Empfehlung zur Verwendung von OpenTofu geben.


Codebeispiel

data "fict_datacenters" "primary" {}

resource "fict_block_storage" "os_disk" {
    type = "SSD"
    size = 50
    name = "devbox-os"
}

resource "fict_virtual_machine" "primary" {
    cpu        = 4
    memory     = 8
    disk       = fict_block_storage.os_disk.id
    datacenter = data.fict_datacenters.primary.ids[0]
}

Note:

Wir springen direkt mal ins kalte Wasser, und schauen uns ein fiktionales Codebeispiel für Terraform an. Was wir hier sehen ist die eigene DSL von HashiCorp, die HashiCorp Configuration Language "HCL". Ich habe mal ein paar Resourcen frei erfunden, die der Verwendung in der Realität gar nicht so unähnlich sind. Wir sehen drei verschiedene Blöcke.

Hierbei sucht der erste Block ein paar Informationen, die wir später verwenden wollen, und die zwei weiteren Blöcke definieren jeweils eine "physische" Resource.

In den letzten Zeilen sehen wir zudem, dass der dritte Block jeweils den ersten und zweiten Block als Argument referenziert, also die Attribute die von den ersten Blöcken bereitgestellt werden weiterverwendet.


HCL - Blöcke

blocktype "provider_label" "reference_label" {
    argument1 = "stringvalue"
    argument2 = 1234
    argument3 = ["list", "of", "same", "types"]
    argument4 = { key: "value", map: "types" }
    argument  = {key1: {foo: "bar"}, key2: {power: 9001}} 
    subblock {
        subargument = "foobar"
    }
}

Note:

Blöcke sind eine Standardstruktur in der HCL, und die Konfigurationsdateien für Terraform setzen sich effektiv aus einem Haufen solcher Blöcke zusammen. Diese Blöcke haben immer einen Typ, und bis zu zwei optionale Labels. Die Blocktypen sind durch Terraform vorgegeben, aber bei den meisten Blocktypen kommt dann noch das Provider Label hinzu, welches effektiv vom gerade verwendeten Provider implementiert sein muss, und ein Reference Label, mit dem ich diesen exakten Block später referenzieren kann. In diese Blöcke kann ich nun Argumente mit den üblichen bekannten Datentypen (Strings, Nummern, Listen, verschachtelte Hash-Maps) und teilweise auch weitere verschachtelte Sub-Blöcke packen.


DAGs

directed acyclic graphs


digraph {
    node [shape=box]
    C -> B -> A
    D -> B
    D -> E
    E -> A
    E -> X
    B -> C [style=invis]
    X -> D [style=invis]
    
    {
    rank = same;
    edge [style=invis]
    B -> E
    rankdir = LR
    }
    
    {
    rank = same
    edge [style=invis]
    C -> D
    rankdir = LR
    }
}

Note:

Um zu verstehen wie Terraform eigentlich arbeitet, ist das Konzept der "Gerichteten azyklischen Graphen" wichtig, im englischen sehr oft als "DAG" abgekürzt. Kurzgefasst beschreiben diese Graphen eine Menge an Knoten, hier die Kästchen, und Kanten, hier dargestellt als Pfeile. Die Kanten sind bei diesem Typ Graph gerichtet, definieren also nur die Verbindung von z.B. Knoten C zu Knoten B, aber nicht Knoten B zu Knoten B.


DAGs

directed acyclic graphs


digraph {
    node [shape=box]
    C -> B
    B -> A [color=darkgreen]
    D -> B [color=darkgreen]
    D -> E [color=darkgreen]
    E -> A [color=darkgreen]
    E -> X
    B -> C [color=red]
    X -> D [color=red]
    
    {
    rank = same;
    edge [style=invis]
    B -> E
    rankdir = LR
    }
    
    {
    rank = same
    edge [style=invis]
    C -> D
    rankdir = LR
    }
}

Note:

Der Zusatz "azyklisch" bedeutet hier, dass ich keine Kreise bauen darf. Es ist also nicht erlaubt wie hier einen Kreis zwischen B und C zu bauen, oder auch über mehrere Knoten hinweg, wie hier von D zu E zu X und wieder D. Hierbei ist aber die Richtung der Kanten entscheidend! Die Verbindung von D zu A über B und E sieht zwar auf den ersten Blick auch wie ein "geschlossener Kreis" aus, kann aber durch die gerichteten Kanten nicht im Kreis abgelaufen werden.


DAGs im Beispiel

digraph {
    node [shape=box]
    vm [label="fict_virtual_machine.primary"]
    disk [label="fict_block_storage.os_disk"]
    dcs [label="data.fict_datacenters.primary"]
    
    vm -> disk [label="id"]
    vm -> dcs [label="ids[0]"]
}

Note:

Hier nun einmal unser Codebeispiel in HCL von eben, so wie Terraform es interpretiert. Hier kann man sehr schön sehen, dass sich die die beiden Resourcen für die Virtuelle Maschine und den Blockstorage, sowie die Datasource für die Datacenter sehr schön als Knoten abbilden lassen. Und die Referenzen auf bestimmte Attribute eines anderen Knoten lassen sich prima als Kante darstellen. Wieso ist das von Vorteil? Terraform hat durch diese Kodifizierung die Möglichkeit, selbstständig die notwendige Reihenfolge für Aktionen zu ermitteln. Für solche Graphen existieren simple Algorithmen, die es erlauben den Graph abzuwandern und zu "lösen".


Vom DAG zum Ausführungsplan

digraph {
    node [shape=box]
    vm [label="fict_virtual_machine.primary",xlabel="second"]
    disk [label="fict_block_storage.os_disk",xlabel="first"]
    dcs [label="data.fict_datacenters.primary",xlabel="first"]
    
    vm -> disk [label="id"]
    vm -> dcs [label="ids[0]"]
}

Note:

Terraform schaut sich diesen Graphen an, und pickt die Knoten raus, die keine Abhängigkeiten auf andere Knoten haben. diese Knoten sind dann für sich lösbar. In unserem Beispiel wäre das der fictional Block Storage, und die Datenquelle zum fictional Datacenter. Sobald diese Knoten abgearbeitet sind kann Terraform die nächsten Knoten bearbeiten, die nur auf diese schon fertigen Knoten referenzieren. In unserem Kleinen Beispiel ist das die Virtuelle Maschine.

Terraform würde hier also zuerst den Block Storage und die Datacenter Quelle abarbeiten, danach die Virtuelle Maschine.


Ansible

  • Plays haben eine feste Reihenfolge der einzelnen Tasks
  • Nutzende müssen selber "richtig" sortieren

Terraform

  • Die Reihenfolge der Blöcke in HCL ist komplett bedeutungslos
  • Terraform sortiert sich die Handlungen selbst passend

Note:

Wenn man diese Abbildbarkeit in Graphen betrachtet erkennen Ansible-Nutzende vielleicht einen schönen Vorteil: In Terraform kann man einfach drauf los schreiben, die Reihenfolge meiner Blöcke in den Dateien ist egal. Terraform findet durch diese Referenzen selbst heraus welche Komponente erst nach einer anderen abgearbeitet werden kann. Bei Ansible muss ein Mensch das selber sortieren, was manchmal sehr undankbare Arbeit ist.


Zustandsebenen

  • Code - geschrieben als HCL
  • "Realität" - eine Resource, die irgendwie existiert
  • Statefile - Terraforms Gedächtnis

Note:

Um zu verstehen wie Terraform einen solchen Graph abwandert und "löst", müssen wir ein weiteres Konzept von Terraform kennenlernen. Es ist recht offensichtlich, dass unsere Infrastruktur als Beschreibung im Code existiert. Nachdem Terraform so seine Dinge getan hat, gibt es dann diese Infrastruktur auch in der "Realität", also da dümpelt dann zum Beispiel irgendwo eine Virtuelle Maschine herum.

FRAGMENT

Zusätzlich dazu gibt es dann aber auch noch ein sogenanntes Statefile. Das ist grob gesagt das Gedächtnis von Terraform. Nachdem Terraform einen Durchlauf gemacht hat, speichert es hier alles was es über die Infrastruktur weiß, die es für uns angelegt hat. Wieso ist das praktisch?


Was sollen wir thun, Fisch?

Code Realität Statefile Aktion
🌟 Erstellen
😎 Nichts.
🔨 Zerstören
💫 Neu erstellen.

Note:

Aus den drei Zustandsebenen kann man eine Matrix an möglichen Aktionen für das "lösen" eines Graphknoten erstellen. In den meisten Fällen ist das Verhalten von Terraform recht trivial nachvollziehbar. Wenn eine Resource nur im Code existiert, dann wird Terraform sie für uns erstellen. Nachdem sie erstellt wurde existiert sie in der Realität und im Statefile. Startet man Terraform erneut wird es nichts tun - denn es ist ja schon alles so, wie es gewünscht wurde.

Entferne ich nun die Definition aus dem Code, wird Terraform sich dank des Statefiles daran erinnern dass diese Resource ja noch in der Realität existiert, und sie netterweise für uns zerstören und aufräumen, sodass die Resource danach weder in Code, Realität noch Statefile weiterexistiert.

Sollte die Resource aus irgendeinen Grund in der Realität zerstört werden - z.B. auf "Löschen" mausgerutscht - wird Terraform sie für uns einfach neu erstellen.


Ansible

  • Tasks können üblicherweise $dinge erstellen.
  • Im Normalfall idempotent, macht also nichts wenn nicht nötig

...aber!

  • Ansible hat kein Gedächtnis
  • Es werden explizite Aufräum-Tasks benötigt

Note:

Ich ziehe hier jetzt noch einmal den Vergleich zu Ansible, um hervorzuheben wieso das verdammt cool ist. In den ersten beiden Fällen unterscheiden sich Ansible und Terraform nicht wirklich. Der Dritte Fall, das zerstören, ist aber ein leidiges Problem bei Ansible. Im Gegensatz zu Terraform hat Ansible kein Gedächtnis, und wenn wir möchten dass ein Task quasi Rückgängig gemacht wird, müssen wir dies explizit definieren. Wenn wir den Task aus dem Code Löschen passiert einfach gar nichts. Stattdessen muss neuer, anderer Code geschrieben werden, und dann auch noch die Ausführungsequenz sowie der Abhängigkeitsgraph manuell auf den Kopf gestellt werden. Meiner Erfahrung nach wird sich diese Mühe meist einfach gar nicht gemacht, da es leichter ist eine VM einfach wegzuwerfen.


😵 Was sollen wir thun, Fisch?!

Code Realität Statefile Aktion
😧 Duplikat erstellen.
👧 Verwaiste Infrastruktur.

Note:

In der Matrix gibt es natürlich noch einige weitere Kombinationen. Mit dem Statefile kommen ein paar Edge Cases hinzu, die ich hervorheben will. Dieses Statefile ist wirklich wichtig. Ihr dürft es niemals verlieren. Passiert das aber doch, kann zum Beispiel der erste Fall eintreten: Terraform denkt, dass keine Infrastruktur existiert, und wird versuchen die gesamte Infrastruktur nocheinmal zusätzlich zu der eigentlich schon bestehenden zu erstellen. Die alte Infrastruktur modert im schlimmsten Fall dann unbemerkt vor sich her, und finanziert die nächste Yacht für Jeff Bezos.

Ihr solltet ein Terraform Projekt auch auf keinen Fall einfach nur vom Dateisystem löschen. Denn wenn der Code und das Statefile nicht mehr existiert, wird wie im ersten Fall die schon bestehende Infrastruktur einfach verwaist, und läuft im Hintergrund weiter.


Arbeiten im Team

Es darf immer nur ein Statefile geben. Challenge im Team: Statefile synchron halten.

Lösung

State Backends! Automatischer down/upload auf einen geteilten Speicher Locks um parallele Ausführung zu verhindern S3, HTTP, Datenbanken, ...

Note:

Kurzer Hinweis zum Arbeiten im Team - Es darf immer nur ein Statefile geben. Wenn ihr das z.B. im Git commited und dann zwei Leute parallel damit arbeiten passieren recht sicher komische Dinge.

Die Lösung hier ist ein state backend. Dabei wird Terraform vor und nach jeder Ausführung das Statefile runter und wieder hochladen, damit alle mit dem selben Statefile arbeiten. Zudem unterstützen manche Backends auch Locks, die verhindern, dass mehrere Leute gleichzeitig Terraform ausführen.


Programmablauf

  1. Code parsen
  2. Realität erfassen & State erneuern
  3. Änderungen erkennen & Plan erstellen
  4. Plan durch User bestätigen lassen
  5. Plan ausführen
  6. State erneuern

Note:

Wie kommt Terraform jetzt zu diesen Schlüssen, und wie läuft so eine Ausführung ab? Grob zusammengefasst wird Terraform euren Code Parsen. Damit ist die erste Spalte in der eben gezeigten Matrix verfügbar. Dann wird Terraform alle Infrastruktur die im State bekannt ist oder per Datasource im Code definiert wurde abklopfen, und den State erneuern.

Damit sind alle Informationen vorhanden um etwaige Änderungen zu erkennen, und dann einen Plan zu erstellen was Terraform tun muss um zum Wunschzustand zu kommen.

Dieser Plan wird komplett vorher generiert, und kann dann von einem User kontrolliert und bestätigt werden. Erst danach wird Terraform Änderungen an der Infrastruktur vornehmen!

Danach wird Terraform alle neuen und veränderten Informationen in den State zurückschreiben, und der Durchlauf ist beendet.


Provider

Terraform hat keine Ahnung von eurer Cloud. Terraform orchestriert Provider. Provider interagieren mit eurer Cloud oder Infrastruktur.

Schaut auf registry.terraform.io

Note:

Das schöne bei Terraform ist, dass Terraform selber eigentlich keine Clouds oder sonstige Infrastrukturen kennt. Terraform an sich implementiert erstmal nur die DSL, States, das ganze Graphen-Geraffel und ähnliches. Aber es hat zum Beispiel keine Idee wie man mit Openstack spricht.

Diesen Teil übernehmen bei Terraform die "Provider". Provider sind eigenständige Programme, die über ein spezielles Protokoll mit einem Terraform Prozess kommunizieren können. Provider sind für sich total dumm, und implementieren nur Funktionen um Resourcen zu lesen, erstellen, löschen oder zu verändern.

Die gesamte Logik steckt also in Terraform, und Provider bilden ein Interface zwischen der Logik und einer beliebigen Cloud oder Infrastruktur.


There's a Provider for that!

  • Infrastruktur - Clouds, DNS, CDNs ...
  • Applikationen - Docker, Authentik, GitLab, GitHub, ...
  • Weird Stuff - Domino's Pizza, Spotify Playlists, ...
  • was auch immer deine Imagination hergibt!

Note:

Die Fülle an Terraform Providern ist wirklich groß. Für die gängigsten APIs gibt es relativ wahrscheinlich schon Provider. Da sind natürlich die üblichen verdächtigen Clouds, DNS Anbieter und CDNs enthalten, es gibt aber auch viele Provider auf Applikationen. Man kann mit Terraform auch Docker Container verwalten, Nutzer in Authentik definieren, und Projekte & Gruppen in GitLab und GitHub definieren.

Wenn man in den komischen Ecken der Registry landet ploppen auch recht verwirrende Nutzungen von Terraform auf, z.B. gibt es einen Provider für Domino's Pizza mit dem man sich tatsächlich eine Pizza bestellen kann, oder ein Provider der einzelne Songs in einer Spotify Playlist verwaltet.

Der Grund für diese Vielfalt ist, dass es nicht schwer ist einen Terraform Provider zu schreiben. HashiCorp stellt ein Beispiel bereit, mit dem man in wenigen Schritten seinen ersten Provider bauen kann. Die einzige Vorraussetzung ist minimale Go Kenntnisse. Ich habe für meine Lohnarbeit einen Terraform Provider geschrieben, und währenddessen Go gelernt. Bis zur ersten lauffähigen Version hat es in etwa eine Woche gedauert, nach circa zwei Wochen war der Provider im produktiven Einsatz.


Stein auf Stein

Große Projekte werden schnell unübersichtlich. Üblicherweise werden sie in einzelne Module aufgebrochen. Module lassen sich kombinieren und wiederverwenden.

Note:

Wenn man irgendwann mal in größeren Projekten arbeitet, wird es schnell unübersichtlich. Die Möglichkeit jeden einzelnen Aspekt der gesamten Infrastruktur in einer Codebasis zu definieren ist zwar charmant, aber wenn man alles in nur eine Datei schmeißt hat man eine kaum überblickbare Müllhalde.

In Terraform geht man deshalb üblicherweise hin und kombiniert logische Schichten zu einzelnen Modulen. So wird häufig alles was mein Netzwerksetup angeht in ein einzelnes Modul verkapselt.


Module

Ein Ordner voller Terraform Code. Optionale variable Definitionen als Inputs Optionale output Definitionen als Outputs

Note:

Module sind am Ende des Tages nur ein Ordner, in dem Dateien mit Terraform Code liegen. Dadrin können beliebige Resourcen definiert werden.

Damit man ein wenig steuern kann wie exakt diese Resourcen aussehen, kann man 'variable' Blöcke schreiben, die einen Input zum Code definieren. Wenn man ein Modul aufruft kann man den Wert dieser Variable festlegen und somit den Code im Modul beeinflussen.

Ein Modul kann auch 'output' Blöcke enthalten, welche bestimmte Attribute von Resourcen im Modul nach außen erreichbar machen.

Ganz wichtig: Ein Modul ist ein für sich gekapseltes Konstrukt. Ich kann nicht aus meinem weiteren Code einfach direkt in ein Modul "hineingreifen" und eine beliebige Resource referenzieren. Module werden immer über ihr nach außen definiertes Interface mit Variablen und Outputs verknüpft.

Man kann sich das so ein bisschen vorstellen wie public und private bei Klassen in OOP.


Minimales Modul

variable "size" {
  type = "number"
  default = 10
}

resource "fict_block_storage" "primary" {
  size = var.size
}

output "id" {
  value = fict_block_storage.primary.id
}

Note:

Hier mal ein minimales Modul. Es wird eine Variable namens "size" definiert, die eine Nummer akzeptiert. Sollte von außen kein Input definiert werden, gilt der default Wert von 10.

Des weiteren gibt es eine fiktionale Block Storage Resource, bei der wir sehen können dass diese Variable direkt als Wert für das size Argument verwendet wird.

Am Ende wird dann noch ein Output namens "id" definiert, welcher das ID Attribut des fiktionalen Block Storage aus dem Modul heraus sichtbar macht.


Modul Verwendung

module "osdisk" {
  source = "./modules/fict_disk"
  size   = 25
}

resource "fict_virtual_machine" "primary" {
  ...
  disk = module.osdisk.id
}

Note:

Um dieses Modul zu verwenden kann ich einen "module" Block benutzen. Bei diesem Block muss man nur ein Referenz-Label angeben, und als Argument die "source" definieren. In diesem Fall ist das ein relativer Pfad zum Ordner der den Module Code enthält. Man sieht, dass die Module als Unterordner zu dieser Datei angelegt wurden, das ist kein Problem. Module beinhalten immer nur den Code auf einer Ordnerebene, und können keine Unterordner beinhalten. Das wären dann separate Module.

An diesem Modul Block kann ich dann in weiteren Argumenten die ganzen Variablen die im Modul definiert wurden mit Werten versehen.

In meinem weiteren Code können dann die Resourcen, hier zum Biespiel eine Virtuelle Maschine, auf die Outputs des Moduls zugreifen, indem die Attribute des Moduls referenziert werden. Erinnerung: Wir können nicht direkt auf die Resourcen im Modul referenzieren, die sind abgekapselt. Alles was wir referenzieren möchten muss als Output definiert sein, und "öffentlich" gemacht werden.


It's Modules, all the way down

digraph {
    rankdir = LR
    graph [compound=true]
    node [shape=box]
    
    subgraph cluster_root {
       label = "module.root" 
        vm [label="fict_virtual_machine.primary"]
        vm -> output [lhead=cluster_disk]
        
        subgraph cluster_disk {
           label = "module.osdisk"
           bs [label="fict_block_storage.primary"]
           output [label="output.id"]
           output -> bs
        }
    }
}

Note:

Hier einmal das Code Beispiel als Graph repräsentiert. Als erstessieht man, dass eine Terraform Konfiguration immer ein Modul ist. Das "oberste" Stück Code, das ich mit Terraform ausführe ist an sich auch immer ein Modul, das Root Modul. In unserem Beispiel hat dieses Modul nur die fiktive VM Resource, welche von dem gesamten OSdisk Modul abhängt. Hier wird Terraform erst das gesamte Modul in sich auflösen, bevor es dann mit der fiktiven VM weitermacht. Das Modul wird wie eine eigene Einheit behandelt.


Logik & Schleifen

resource "fict_virtual_machine" "maybe" {
  count = var.enable ? 1 : 0
    ...
}

output "machine_ids" {
  value = [for vm in fict_virtual_machine.maybe : vm.id]
}

output "machine_id" {
  value = var.enable ? fict_virtual_machine.maybe[0].id : null
}

"Hack" um Resourcen an oder auszuschalten

Note:

Aus diversen Einschränkungen des internen Konzeptes von Terraform ist es nicht sinnvoll möglich eine Resource einfach direkt ein oder ausschaltbar zu machen. Vielleicht will ich ja in meinem Modul die Möglichkeit geben eine bnestimmte Infrastrukturkomponente zu erstellen, oder nicht, je nach Einsatzzweck. Damit würde man dann aber potenziell Abhängigkeiten ins "Nichts" erlauben.

Stattdessen verwendet man für solche Fälle gerne das Meta-argument "count", das an jede Resource gehängt werden kann. Terraform repräsentiert diesen Block dann nicht als einzelne Resource, sondern eine Liste von Resourcinstanzen. Und diese kann eben Null oder Ein Element enthalten, so wie hier im Code gezeigt. Hier wird der Wert für Count mit einem ternären Operator erzeugt, also eine Kurzform von if/else, die man vielleicht auch aus anderen Programmiersprachen kenn. Sollte var.enable Wahr sein, wird der Wert links des Doppelpunkt genommen, also Eins, ansonsten rechts des Doppelpunkt, Null.

Um nun meine ID auszugeben muss ich entweder wieder eine Liste repräsentieren, oder den Ausgabe Wert optional nullen. Je nach Einsatzzweck kann das eine oder andere eleganter sein.


for_each

locals {
  # Locals sind frei definierbare Zwischenwerte
  machines = ["red", "green", "blue"]
}

resource "fict_virtual_machine" "ducks" {
  # for_each akzeptiert set(string) oder maps
  for_each = local.machines
  ...
}

Note:

Sehr wichtig ist auch das for_each Meta-Argument. Mit diesem kann man auch eine Menge an Resource-Instanzen erstellen, jedoch werden diese nicht als geordnete Liste referenziert, sondern in einer Hash-Map - es gibt zu jeder Instanz einen vorgegebenen Identifier. Das kann entweder ein einfacher String aus einem Set sein, oder der Key in einer Map, wobei dann das Value der Map in der Resource weiter referenziert werden kann, um die Instanzen mit unterschiedlichen Argumenten zu füttern.


Wieso for_each?

count

count wird Resourcen nach dem geänderten Index modifizieren oder neu erstellen.

Index Vorher Nachher
0 Rot ↑ Grün 💫
1 Grün ↑ Blau 💫
2 Blau 🔨

Note:

In den meisten Fällen ist for_each die bessere Wahl, wenn man explizit eine Menge von verschiedenen Resource-Instanzen erstellen will. Macht man dies mit dem Ablaufen einer Liste, also so wie man im programmieren klassisch eine For-Schleife mit hochzählen eines Integers kennt, bekommt man in der Zukunft Probleme.

Will ich nämlich ein Element aus dieser Liste entfernen, rutschen alle Werte eine Stelle nach vorne. Dadurch verändert sich aus der Sicht von Terraform der Wert für jedes Element in der Liste danach, und Terraform wird versuchen diese Resourcen entweder zu modifizieren oder neu zu erstellen. Terraform erkennt nicht, dass ja schon passende Resourcen existieren, die enfach "aufrutschen" könnten.


Wieso for_each?

for_each

for_each entfernt nur den entsprechenden Eintrag aus der Map

Key Vorher Nachher
red Rot 🔨
green Grün Grün
blue Blau Blau

Note:

Wählt man stattdessen for_each, referenziert Terraform diese Instanzen nicht über ihre Position in ihrer Liste, sondern über einen Key, welcher pro Instanz einzigartig sein muss. Lösche ich nun eine Instanz, kann Terraform auch einfach nur diesen Key aus der Map entfernen, und es wird nichts weiter getan.


Zusammenfassung

Terraform ist toll.


Zusammenfassung

  • Jeder Ordner mit Terraform HCL Dateien ist ein Modul.
  • Man schreibt für jede Komponente die man hat einen Resource Block.
  • Abhängigkeiten untereinander werden durch Referenzen ermittelt.
  • Abhängigkeiten geben die Ausführungsreihenfolge an.
  • Verliert niemals das Statefile.
  • Es gibt für sehr viele Dinge einen Provider - falls nicht, DIY!
  • Im Zweifel immer for_each nutzen.

Workshop

Morgen, 12:30 hier.

Bringt euren Laptop mit.

Lustiges experimentieren mit Terraform.


Fragen? Fragen!

Folien: https://git.jankoppe.de/j/kh24-tf-talk