Terraform AWS Speed Up

Terraform: AWS Runtime Speed Up

Avatar von Andreas Rudat

Die Ausführung von Terraform ist nicht gerade für Geschwindigkeit berühmt, dennoch gibt es ein paar Möglichkeiten, Terraform Beine zu machen. Für AWS Provider gibt es z. B. ein paar kleine Tweaks, die man einfach nur konfigurieren muss. Ähnliches gibt es sicher auch für andere Provider.

Terraform-Optionen deaktivieren

Die folgenden Optionen bringen nur dann eine zeitliche Verbesserung, wenn es sich um ein größeres und komplexeres Terraform-Projekt handelt, in dem entsprechend viel API-Kommunikation stattfindet. Das typische „Tach Welt“ wird da keinen Unterschied zeigen.

Die Einstellungen werden in der AWS-Provider-Definition in Terraform hinterlegt.

skip_metadata_api_check = false

Das können wir verwenden, wenn wir keine EC2-Instanzen anzapfen wollen. Aus Kostengründen möchte man diese Instanzen heute nur da verwenden, wo es unbedingt nötig ist. Sie sind im Verhältnis zu Containern einfach teurer.

Was genau wird hier aber verhindert? Das Metadata API ist ein Endpunkt, um Meta-Information von EC2-Instanzen auszulesen. Also Dinge wie host name, security groups, usw. Wenn diese Informationen im Terraform-Kontext nicht benötigt werden, sollte man es abschalten. Das spart Requests und damit Wartezeit.

skip_credentials_validation = false

Wenn Session Tokens verwendet werden und keine statischen Access Keys, kann diese Validierung deaktiviert werden. Denn der STS-Service ist genau nur für statische Credentials, den wir dann also unnötig anpingen und damit wieder auf Antworten warten.

skip_region_validation = false

Damit können Pseudo-Regionsbezeichnungen bzw. alternative Namen verwendet werden, wenn der AWS Provider einen ähnlichen Cloud-Anbieter verwendet. MinIO ist ein Beispiel dafür, der Service verwendet ein S3-kompatibles API.

Run to the hills …

… with parallelism. Wenn Terraform mit einem Provider kommuniziert, tut es das in der Regel über APIs. Die Schlussfolgerung ist also die Folgende: stelle ich ganz viele Anfragen gleichzeitig an meinen Endpunkt, dann sollten auch ganz viel gleichzeitig parallel abgearbeitet werden. So die Theorie.

Praktisch hängt es wieder vom Projekt ab. Wenn Ressourcen beim Setup lange brauchen und deswegen andere Abhängigkeiten blockiert sind, helfen natürlich auch 100 gleichzeitige Requests nichts. Dennoch kann es Sinn ergeben, an der Parallelisierung der Requests zu schrauben. Ist das eigentliche Setup erstmal durch, könnte es bei späteren Anpassungen die Ausführung beschleunigen.

Was muss ich denn jetzt tun?

Terraform verwendet bei der Ausführung per default 10 parallele Verbindungen. Mit dem Parameter -parallelism können wir diesen Wert erhöhen oder senken.

Ernüchternde Erkenntnisse …

Es gibt einige Tricks, die man anwenden kann. Pauschal kann – wie so häufig – aber keine schnellere Ausführung garantiert werden. 

Da hilft es nur, sich das eigene Projekt genau anzuschauen und auszuprobieren. Die Komplexität im Terraform-Projekt spielt eine ganz wichtige Rolle für die Ausführungszeit, genauso wie in der Softwareentwicklung auch. Damit am Ende eine Versionsnummer als String ausgegeben wird, wollen wir ja auch nicht, dass dafür unnötig 10 APIs angesprochen und ein Backup von der User-Datenbank erstellt werden.

Daher ist es wichtig, ein aufgeräumtes Terraform-Projekt zu haben und den Code in Komponenten aufzuteilen, auch wenn dann in Kauf genommen werden muss, mehr als nur an einer Stelle Terraform ausführen zu müssen. Die Ausführungszeit wird es euch aber danken. 

Zum anderen hängt die Zeit natürlich auch vom verwendeten Provider an der Gegenstelle ab. Wenn der Provider 10 Minuten braucht um einen größeren Service zu bootstrappen oder zu ändern, dann könnt ihr da nichts machen.

Auch die CI/CD Pipelines hätten wir gerne schneller!

Also, was tun? Na Cachen, ganz klar! Aber was kann bei Terraform sinnvollerweise gecached werden?

Der Plugin Cache

Standardmäßig speichert Terraform die Plugins an Ort und Stelle in einem Unterverzeichnis. Wird nun die Variable TF_PLUGIN_CACHE_DIR gesetzt, speichert Terraform alle Plugins (genau genommen die Provider Binaries) dorthin. Es wird ein Link angelegt, wenn ein Provider im Code benötigt wird, anstatt ihn jedesmal herunterzuladen. Es kann also doppelt gespart werden: Zunächst durch Vermeidung von doppelten Dateien, zum anderen durch Cachen des Plugin-Verzeichnisses selbst. 

Dazu noch ein Beispiel, wie es für Gitlab aussehen würde:

cache:
  key:
    files:
      - ${CI_PROJECT_DIR}/.terraform.lock.hcl
  paths:
    - ${CI_PROJECT_DIR}/.plugin-cache

Wir definieren, dass der .plugin-cache so lange gecached bleibt, wie sich nichts an der .terraform.lock.hcl-Datei ändert. Fertig ist die Laube. Wichtig ist hier jedoch, dass das Plugin-Verzeichnis bereits vorhanden ist; Terraform legt es nicht selbständig an!

Klingt simpel und gut, aber Obacht!

Terraform speichert in der Datei .terraform.lock.hcl die Hashes der Provider für die entsprechend verwendete Plattform. Das bedeutet: wenn die hcl unter Linux angelegt wurde, matched sie nicht mit macOS oder Windows und Terraform wird entsprechend den passenden Provider für die Plattform herunterladen und den Hash hinzufügen.

Weshalb kann das ein Problem sein?

Der fehlende Plattform-Support kann unseren eben beschriebenen Caching-Mechanismus kaputt machen. Denn wenn in der CI/CD Pipeline auf einmal ein ARM64 Runner verwendet wird, haben wir nichts von der in Git liegenden .terraform.lock.hcl-Datei und Terraform muss sich die Information noch zusätzlich besorgen. Best Practice ist daher einfach, das .terraform.lock.hcl mit allen Plattformen anzulegen, die im Projekt benötigt werden. Das geht einfach mit dem Befehl

terraform providers lock \
  -platform=darwin_amd64 \
  -platform=darwin_arm64 \
  -platform=linux_amd64 \
  -platform=linux_arm64

Im besten Fall weiß ich also, welche Plattformen lokal und in der CI/CD verwendet werden. Um so weniger Plattformen ich supporten muss, um so weniger Daten Ballast trage ich mit mir beim Cachen herum.

Nun aber das eigentliche Problem: Die Idee war ja, die Provider zu Cachen. Angenommen, im Projekt wird ein Provider herausgenommen beziehungsweise geändert, wie auch immer. Die .terraform.lock.hcl hatte alle Plattformen gespeichert, nun aber beispielsweise ausschließlich für macOS. In der Pipeline werden im ersten Schritt die Provider geprüft und entsprechend lokal aktualisiert.

validate-terraform:
  before_script:
    - "[ ! -d ${TF_PLUGIN_CACHE_DIR} ] && mkdir ${TF_PLUGIN_CACHE_DIR}"
  script:
    - terraform init
    - terraform validate
    - terraform fmt -list=true -write=false -diff=true -check=true -recursive

Da nicht alle Plattformen mit gespeichert wurden, wird hier im Beispiel ARM64 entfernt. Im nächsten Job wird terraform plan ausgeführt … und nun knallt es, weil jetzt erst der Provider wirklich benötigt wird.

Fehlermeldung in Terraform: die benötigten Plugins fehlen.

Der ausgeführte Code sieht so aus:

plan-terraform:
  artifacts:
    public: false
    paths:
      - ${TF_ROOT}/plan.tfplan
  script:
    - terraform plan -out=plan.tfplan -parallelism=50

Unser Runner ist ARM64-basiert, dessen Support wir rausgenommen haben. Ok, bauen wir halt ein terraform init vor dem terraform plan ein, gefixt \o/

… ja, schon. Aber auch ein weiterer run, der verhältnismäßig lange braucht, nur um den einzelnen, fehlenden Provider neu ziehen zu müssen. Das schmälert doch den Vorteil des Cachens.

Lösung für das Problem

Wie oben beschrieben, sollten alle Plattform Provider immer mit gespeichert werden, andernfalls wird ein Job mit allen Befehlen gleichzeitig erstellt. Das bringt natürlich wieder andere Konsequenzen mit sich, auf die ich hier nicht eingehen möchte.

Darf es nocht etwas mehr .terraform sein?

Den Ordner .terraform wollen wir nicht vergessen. Eigentlich ist nur der Modul-Teil in dem Verzeichnis wichtig, weil die Provider cachen wir ja ohnehin schon extra. Es tut hier aber nicht weh, die Links mit zu cachen.

Je nach Projekt gibt es halt ein bis drölf dieser Verzeichnisse … es muss wieder abgewogen werden, was hier Sinn ergibt. Hier gilt ebenfalls: es kommt auf das Projekt an. Der .terraform-Ordner kann ebenfalls woanders abgelegt werden. Gibt es mehrere dieser Ordner im Projekt, kann es zu Problemen kommen, wenn diese an einem Punkt zusammengeführt sind.

Jetzt aber mal laufen lassen, oder?

Last but not least, die übliche Überlegung, wann meine Pipeline überhaupt starten muss Doch nur wenn sich die *.tf oder *.hcl Files ändern.

Wieder anhand des Gitlab-Beispiels:

rules:
  if: $CI_PIPELINE_SOURCE == 'merge_request_event'
  changes:
    - "${TF_ROOT}/*.tf"
    - "${TF_ROOT}/*.hcl"

Fazit

Auch bei CI/CD kommt es mal wieder auf die Situation an, ob und wie sehr man Terraform beschleunigen kann. Das Thema sollte nicht vernachlässigt werden. Niemand mag langsame Pipelines und sie können im schlimmsten Fall auch Geld kosten, wenn dringende Änderungen der Infrastruktur auf Grund dessen nicht gemacht werden können.

Vielen Dank für die Aufmerksamkeit und Happy Tweaking!

Mayflower Cloud Discovery

Ergreife jetzt die Chance und buche einen einstündigen Termin bei einem unserer Cloud-Experten, um einen detaillierten Überblick über deine Cloud-Infrastruktur zu erhalten. Ganz ohne (versteckte) Kosten! » Mehr erfahren

Avatar von Andreas Rudat

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert


Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.