Terragrunt – the missing Terraform Part
Terraform ist eines der Top IaC (Infrastructure as Code) Tools. Mit Terraform können Entwickler sicher und effizient Infrastruktur erstellen, ändern und versionieren. In der deklarativen Konfigurationssprache HCL (HashiCorp Configuration Language) wird der gewünschte Zustand einer cloudbasierten oder lokalen Infrastruktur beschrieben. Anschließend kann Terraform daraus einen Plan erstellen um die Infrastruktur bereitzustellen. Terraform hat viele Vorteile stößt aber in bestimmten Bereichen auf Einschränkungen. Terragrunt erweitert Terraform, indem es das Wiederverwenden und Organisieren von Terraform-Konfigurationen erleichtert. Es bietet eine einheitliche Struktur für Projekte mit mehreren Umgebungen und verbessert den Umgang mit Remote States und Modulen. Zudem kann Terragrunt die Abhängigkeiten zwischen mehreren Modulen automatisch auflösen.
Terraform Projekt Struktur
Vereinfachtes Beispiel einer Projektstruktur mit zwei oder mehr Modulen.
├── environments
│ ├── dev
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ ├── provider.tf
│ │ └── variables.tf
│ └── prod
│ ├── main.tf
│ ├── outputs.tf
│ ├── provider.tf
│ └── variables.tf
└── modules
├── ec2
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── network
├── main.tf
├── outputs.tf
└── variables.tf
In reinen Terraform Projekten wird für jede Umgebung ein separates Environment Modul (dev, prod) erstellt, in das die geteilten Module (ec2, network) geladen werden. Da die unterschiedlichen Umgebungen bis auf die geteilten Module keine gemeinsame Codebasis haben entstehen sehr leicht Abweichungen zwischen den redundanten Code Teilen. Die Dateien (main.tf, outputs.tf, provider.tf, variables.tf)
werden meist doppelt oder mehrfach gepflegt.
Terragrunt Projekt Struktur
Vereinfachte Projektstruktur mit zwei oder mehr Modulen.
|── live
| ├── terragrunt.hcl
| ├── _env
| │ ├── ec2.hcl
| │ ├── network.hcl
| │ └── provider.hcl
| ├── prod
| │ ├── env.hcl
| │ ├── app
| │ │ └── terragrunt.hcl
| │ └── mysql
| │ └── terragrunt.hcl
| └── dev
| ├── env.hcl
| ├── ec2
| │ └── terragrunt.hcl
| └── network
| └── terragrunt.hcl
└── modules
├── ec2
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── network
├── main.tf
├── outputs.tf
└── variables.tf
Mit Terragrunt können Code Duplikate zwischen mehreren Umgebungen sehr effektiv vermieden werden. Im Ordner _env
wird die gemeinsame Code Basis parametrisiert definiert. Anschließend werden die Code Module aus _env
in den Umgebungen (prod, dev) geladen und mit den Umgebungsspezifischen Parametern aus <env>/env.hcl
konfiguriert.
Nachteile von Terraform
Wiederverwendbarkeit und Modularität
Terraform bietet die Möglichkeit Code in Modulen Wiederverwendbar zu organisieren. Allerdings wird das Verwalten und Verwenden vieler verschiedener Module in komplexen Infrastrukturumgebungen mit mehren Stages, wie z.B. Dev, Test und Prod, in der Handhabung herausfordernd.
State File Management
Terraform speichert den aktuellen Status der gesamten Infrastruktur in einem gemeinsamen state file. In Umgebungen mit mehreren Modulen und Teams kann es durch ein zentrales state file zu Probleme kommen. Wenn mehrere Teammitglieder an verschiedenen Modulen arbeiten kommt es so leicht zu Race Conditions bei denen das State File zeitweise gelockt wird.
Code Duplikation und Konfiguration verschiedener Umgebungen
Terraform hat keine native Unterstützung für die automatische Konfiguration über Umgebungen hinweg (z.B. dev, staging, production). Der Entwickler muss selbst komplexe Lösungen mit Variablen oder anderen Mechanismen bauen. Bei der Arbeit in mehreren Umgebungen neigt man dazu, Konfigurationen zu duplizieren, da Terraform keine out-of-the-box Lösung für den Umgang mit mehreren Umgebungen bietet. Dies führt zu redundanten Konfigurationen und erschwert die Wartung.
Vorteile von Terragrunt als Wrapper
Terragrunt ist ein Wrapper um Terraform und adressiert einige dieser Schwächen:
Verwaltung von Umgebungen und DRY-Prinzip
Terragrunt unterstützt eine bessere Strukturierung von Terraform-Code, indem es das DRY-Prinzip („Don’t Repeat Yourself“) fördert. Anstatt Konfigurationen für verschiedene Umgebungen zu duplizieren, bietet Terragrunt Mechanismen, um Variablen und Infrastruktur für verschiedene Umgebungen effizient zu verwalten. Es vereinfacht die Handhabung von gemeinsamen Konfigurationen, indem man eine einzige Konfigurationsdatei nutzt und nur umgebungsspezifische Werte überschreibt.
Docs: https://terragrunt.gruntwork.io/docs/features/keep-your-terraform-code-dry/
Dependency Management
In komplexen Terraform-Setups gibt es oft Abhängigkeiten zwischen verschiedenen Modulen (z.B. ein Netzwerk muss vor einer Datenbank bereitgestellt werden). In Terragrunt können Abhängigkeiten zwischen Modulen explizit definiert werden um diese automatisch in der richtigen Reihenfolge auszuführen.
Docs: https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#dependency
Automatisiertes State Management- und Provider-Konfiguration
Anstatt in jedem Modul dieselbe State Backend- und Provider-Konfiguration zu duplizieren, kann Terragrunt diese Konfiguration automatisch einfügen. Das reduziert redundanten Code in Terraform-Konfigurationsdateien und vereinfacht die Verwaltung.
State Backend Konfiguration
Terragrunt unterstützt die automatische Verwaltung von state files für verschiedene Module und Umgebungen. Terragrunt kann state files pro Umgebung und Modul isolieren, indem es sie beispielsweise automatisch in unterschiedlichen S3-Buckets speichert (für AWS) oder in geeignete Storage-Lösungen wie Gitlab Remote State Backend integriert.
Im folgenden Beispiel wird ein Template für das Gitlab HTTP Backend definiert.
locals {
// read the env vars
env_locals = read_terragrunt_config(find_in_parent_folders("${get_original_terragrunt_dir()}/../env.hcl"))
// evaluate the gitlab url
gitlab_url_eval = can(local.env_locals.locals.gitlab_url) ? local.env_locals.locals.gitlab_url : "https://gitlab.com"
gitlab_project_id = local.env_locals.locals.gitlab_project_id
}
remote_state {
backend = "http"
generate = {
path = "backend_http_gitlab.tf"
if_exists = "overwrite_terragrunt"
}
config = {
address = "${local.gitlab_url_eval}/api/v4/projects/${local.env_locals.locals.gitlab_project_id}/terraform/state/${local.env_locals.locals.env}_${basename(get_original_terragrunt_dir())}"
lock_address = "${local.gitlab_url_eval}/api/v4/projects/${local.env_locals.locals.gitlab_project_id}/terraform/state/${local.env_locals.locals.env}_${basename(get_original_terragrunt_dir())}/lock"
unlock_address = "${local.gitlab_url_eval}/api/v4/projects/${local.env_locals.locals.gitlab_project_id}/terraform/state/${local.env_locals.locals.env}_${basename(get_original_terragrunt_dir())}/lock"
lock_method = "POST"
unlock_method = "DELETE"
retry_wait_min = 5
username = "${get_env("TF_HTTP_USERNAME")}"
// the env variable TF_HTTP_PASSWORD is evaluated at runtime, i do not recommend to save it for security reasons
}
}
Docs: https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#remote_state
In der root terragrunt.hcl
Datei wird das in env.hcl
definierte Terraform Backend geladen.
locals {
env_locals = read_terragrunt_config(find_in_parent_folders("env.hcl"))
tflint_hook_enabled = get_env("DISABLE_TFLINT_HOOK", "false") == "true" ? false : true
trivy_hook_enabled = get_env("DISABLE_TRIVY_HOOK", "false") == "true" ? false : true
backend_target_evaluation = can(local.env_locals.locals.state_backend) ? local.env_locals.locals.state_backend : "local"
backend_target = contains(["http_gitlab", "local"], local.backend_target_evaluation) ? local.backend_target_evaluation : "local"
backend_config = read_terragrunt_config(find_in_parent_folders("backend_${local.backend_target}.hcl"))
}
remote_state {
backend = local.backend_config.remote_state.backend
config = local.backend_config.remote_state.config
generate = local.backend_config.remote_state.generate
}
Code: https://github.com/csautter/terragrunt-blueprint/blob/main/deployments/terraform/terragrunt.hcl
Provider Konfiguration
Terragrunt ermöglicht eine vereinfachte Provider Konfigurationen in verschiedenen Umgebungen und Modulen. Dabei können die Provider Konfigurationen automatisch abhängig von der aktuellen Umgebung erzeugt werden.
Im Beispiel von provider_aws_config.hcl
wird eine AWS Provider Konfiguration generiert. Alle Parameter werden aus der env.hcl
Datei der jeweiligen Umgebung (dev, staging, prod) geladen.
locals {
env = read_terragrunt_config(find_in_parent_folders("env.hcl"))
}
generate "aws" {
path = "provider_aws.tf"
if_exists = "overwrite_terragrunt"
contents = <<-EOF
provider "aws" {
default_tags {
tags = {
Environment = "${local.env.locals.env}"
}
}
region = "${local.env.locals.region}"
profile = "${get_env("AWS_PROFILE", "${local.env.locals.aws_profile}")}"
allowed_account_ids = ["${get_env("AWS_ACCOUNT_ID", "${local.env.locals.aws_account_id}")}"]
}
EOF
}
Die AWS Provider Konfiguration kann anschließend mit einem include Block in den Modulen geladen werden.
include "provider_vault_config" {
path = "${get_terragrunt_dir()}/../../_env/provider_aws_config.hcl"
expose = true
}
Docs: https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#include
Hooks
In Terragrunt können Hooks definiert werden die z.B. trivy, tflint oder terraform fmt bei jeder Ausführung von terraform plan/apply getriggert werden. Durch das sofortige Feedback bei der Entwicklung, kann die Code Qualität deutlich erhöht werden.
terraform {
before_hook "terraform_fmt" {
commands = ["apply", "plan"]
execute = ["terraform", "fmt", "-recursive"]
}
before_hook "terragrunt_hclfmt" {
commands = ["apply", "plan"]
execute = ["terragrunt", "hclfmt"]
}
before_hook "tflint" {
commands = local.tflint_hook_enabled ? ["apply", "plan"] : []
execute = ["tflint"]
}
before_hook "trivy" {
commands = local.trivy_hook_enabled ? ["apply", "plan"] : []
execute = ["trivy", "config", "."]
}
}
Code: https://github.com/csautter/terragrunt-blueprint/blob/main/deployments/terraform/terragrunt.hcl
Docs: https://terragrunt.gruntwork.io/docs/features/hooks
Fazit
Terraform ist ein sehr leistungsfähiges Tool zur Verwaltung von Infrastruktur, stößt jedoch bei der Handhabung von State-Dateien, der Modularität und der Verwaltung von Umgebungen an Grenzen. Terragrunt wurde entwickelt, um diese Schwächen zu beheben, insbesondere durch Verbesserungen bei der Wiederverwendbarkeit, dem State Management und dem Umgang mit Abhängigkeiten. Durch den Einsatz von Terragrunt in meinen Projekten konnte ich die Code Qualität deutlich erhöhen und Komplexität reduzieren.