Terraform & Terragrunt

Terraform ist ein Infrastructure-as-Code-Tool (IaC), mit dem du Cloud-Ressourcen deklarativ beschreibst und reproduzierbar bereitstellst. Du definierst die gewünschte Zielarchitektur in .tf-Dateien, Terraform plant die Änderungen (plan) und wendet sie an (apply).

Die Nutzung ermöglicht einem Administrator:

  • Versionierbare Infrastruktur
  • Reproduzierbare Deployments und Drift-Erkennung
  • Remote State-Handling
  • Wiederverwendbare Module mit DRY-Prinzip
  • Zentrale Variablen- und Backend-Verwaltung
  • Klare Ordnerstruktur für Stages/Accounts/Regionen
  • Automatisierte Abhängigkeiten und Remote State-Handling

OpenTofu ist ein vollständig offener Fork von Terraform (kompatibel zu HCL und den meisten Providern/Modulen). Gründe für OpenTofu:

  • Open Source unter einer Community-getriebenen Governance
  • Weiterhin deklaratives IaC, gleiches CLI-Erlebnis (init/plan/apply/destroy)
  • Hohe Kompatibilität zu bestehenden Terraform-Konfigurationen
  • Nahtlose Nutzung mit Terragrunt

Windows

choco install opentofu -y
choco install terragrunt -y

Mac

brew install opentofu
brew install terragrunt

Linux

curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh -o install-opentofu.sh
chmod +x install-opentofu.sh
./install-opentofu.sh --install-method deb
rm -f install-opentofu.sh

Mit Terraform ist es möglich, Docker Images zu bauen und revisionssicher bereitzustellen. In diesem Beispiel wird Nginx in Docker bereitgestellt. Um Docker in Terraform nutzen zu können, muss zunächst der passende Provider konfiguriert werden. Diese werden üblicherweise in einer Datei mit dem Namen provider.tf abgelegt.

provider.tf

terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {
  host = "unix:///var/run/docker.sock"
}

Anschließend kann das Docker Image definiert und ein zugehöriger Container erstellt werden.

main.tf

resource "docker_image" "nginx" {
  name = "nginx:latest"
  keep_locally = true
}

resource "docker_container" "nginx" {
  name  = "demo-nginx"
  image = docker_image.nginx.image_id
  ports {
    internal = 80
    external = 8080
  }
}

Mit Terraform kann das Projekt dann initialisiert werden. Dabei werden unter anderem die Abhängigkeiten von Providern und Modulen aufgelöst. Danach kann der Zustand geplant (plan), angewendet (apply) oder zerstört (destroy) werden.

Bevor etwas angewendet wird, muss dies interaktiv bestätigt werden. Dies kann durch die Anweisung --auto-approve übergangen werden.

tofu init
tofu plan
tofu destroy

DRY: Don't repeat yourself

Um Redundanzen in Code zu vermeiden, können wiederverwendete Ressourcen in Module gekapselt werden. Am Beispiel des Nginx Containers, könnte man das Modul nginx erstellen. Dazu erstelle eine variables.tf für dynamische Variablen, eine main.tf für das Image und den Container, sowie eine outputs.tf, um die resultieren Variablen abzulegen.

modules/nginx/variables.tf

variable "container_name" {
  type        = string
  description = "Name des Containers"
  default     = "nginx"
}

variable "external_port" {
  type        = number
  description = "Externer Port"
  default     = 8080
}

variable "image" {
  type        = string
  description = "Docker Image"
  default     = "nginx:latest"
}

modules/nginx/main.tf

resource "docker_image" "this" {
  name         = var.image
  keep_locally = true
}

resource "docker_container" "this" {
  name  = var.container_name
  image = docker_image.this.image_id

  ports {
    internal = 80
    external = var.external_port
  }
}

modules/nginx/outputs.tf

output "url" {
  value = "http://localhost:${var.external_port}"
}

Zuletzt muss noch die root main.tf das Modul nutzen. So kann der vorhandene Code genutzt werden, um Ressourcen mit dynamischer Konfiguration wiederzuverwerten:

main.tf

module "demo-nginx" {
  source         = "./modules/nginx"
  container_name = "demo-nginx"
  external_port  = 8080
}

module "production-nginx" {
  source         = "./modules/nginx"
  container_name = "production-nginx"
  external_port  = 8081
}

Terragrunt hilft, Variablen, Remote State und Backends zentral zu pflegen und Umgebungen konsistent zu strukturieren.

Als Beispiel sollen die beiden Produktionsumgebungen mit unterschiedlichen Variablen definiert werden, um für verschiedene Umgebungen bereitgestellt zu werden.

Dazu müssen zunächst die Umgebungen definiert werden, als Zentrale Konfigurationsdatei dient die root.hcl:

root.hcl

locals {
 terraform_dir = "${get_parent_terragrunt_dir()}/"
}

terraform {
 source = local.terraform_dir
}

Um die Variablen für die Umgebungen festzulegen, sollten die Variablen in einem separaten Bereich ablegt werden. Dieser Pfad muss später in der terragrunt.hcl referenziert werden.

configuration/local/terraform.tfvars

container_name = "demo-nginx"
external_port  = 8080

configuration/production/terraform.tfvars

container_name = "production-nginx"
external_port  = 8081

Anschließend kann die Konfiguration für die einzelnen Umgebungen definiert werden

environments/local/terraform.hcl

include "root" {
  path = find_in_parent_folders("root.hcl")
}

locals {
  env_path = "${get_parent_terragrunt_dir()}/terragrunt-configuration/local"

  tfvars_files = [
    for file in fileset(local.env_path, "**") :
    "${local.env_path}/${file}"
  ]
}

terraform {
  extra_arguments "variables" {
    commands = get_terraform_commands_that_need_vars()
    
    required_var_files = local.tfvars_files
  }
}

generate "backend" {
  path      = "main.backend.tf"
  if_exists = "overwrite_terragrunt"
  contents = <<EOF
terraform {
  backend "local" {
    path = "${path_relative_to_include()}/tofu.tfstate"
  }
}
EOF
}

generate "provider" {
  path      = "providers.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {
  host = "unix:///var/run/docker.sock"
}
EOF
}

Dazu kann eine eigene Provider-Konfiguration (providers.tf) oder Remote-State (backend.tf) pro Umgebung erstellt werden, falls die Auto-Generierung für die benötigten Zwecke nicht ausreicht.

Anschließend müssen die erwarteten Variablen definiert werden (variables.tf) und die root main.tf überarbeitet werden:

variables.tf

variable "container_name" {
  type = string
}

variable "external_port" {
  type = number
}

main.tf

module "nginx" {
  source         = "./modules/nginx"
  container_name = var.container_name
  external_port  = var.external_port
}

Um nun die Konfiguration für eine Umgebung auszuführen, starte Terraform auf Höhe der terraform.hcl der gewünschten Umgebung:

cd environments/local
terragrunt init
terragrunt plan
terragrunt apply

cd environments/production
terragrunt init
terragrunt plan
terragrunt apply

Jan Schneider