Terragrunt and Terraform

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.

backend_http_gitlab.hcl
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
  }
}

Code: https://github.com/csautter/terragrunt-blueprint/blob/main/deployments/terraform/backend_http_gitlab.hcl

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.

terragrunt.hcl
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.

provider_aws_config.hcl
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
}

Code: https://github.com/csautter/terragrunt-blueprint/blob/main/deployments/terraform/env/_env/provider_aws_config.hcl

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
}

Code: https://github.com/csautter/terragrunt-blueprint/blob/main/deployments/terraform/env/dev/aws_dummy/terragrunt.hcl

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.

Hinterlasse einen Kommentar

de_DEGerman