🏗️ Terraform Lektion 9/15 ~10 Min. Fortgeschritten

Terraform und Kubernetes

K8s Ressourcen und Helm via Terraform.

📝 Meine Notizen

Lernziele

Nach dieser Lektion kannst du den Kubernetes- und Helm-Provider sinnvoll einsetzen und erklären, wann Terraform statt kubectl oder GitOps das richtige Werkzeug ist. Du konfigurierst die Provider-Anbindung an einen Cluster, legst Namespaces und Deployments an und installierst Helm-Releases. Du kennst die Grenzen — insbesondere fehlende Drift-Reconciliation.

Das Problem: zwei Welten, eine Plattform

Als Platform Engineer jonglierst du zwischen Cloud und Cluster. Das VPC und der EKS-Cluster kommen aus dem AWS-Provider — aber wer legt den ingress-nginx-Namespace an, installiert cert-manager und konfiguriert die RBAC-Rollen für Team A? Viele Teams lösen das mit Shell-Skripten, die helm install und kubectl apply hintereinander aufrufen. Das ist imperativ, nicht reviewbar und bricht bei Fehlern in der Mitte.

Terraform kann beide Ebenen in einem Plan vereinen — vorausgesetzt, du verstehst, was es gut kann und was nicht.

Kubernetes-Provider: einzelne Ressourcen deklarativ

Der kubernetes-Provider spricht die kube-API — analog zum AWS-Provider für EC2:

terraform {
  required_providers {
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.30"
    }
  }
}

provider "kubernetes" {
  config_path = "~/.kube/config"
  # oder explizit für CI:
  # host                   = var.cluster_endpoint
  # cluster_ca_certificate = base64decode(var.cluster_ca)
  # token                  = var.cluster_token
}

resource "kubernetes_namespace" "platform" {
  metadata {
    name = "platform-system"
    labels = {
      "pod-security.kubernetes.io/enforce" = "baseline"
    }
  }
}

resource "kubernetes_deployment" "demo" {
  metadata {
    name      = "demo"
    namespace = kubernetes_namespace.platform.metadata[0].name
  }

  spec {
    replicas = 2

    selector {
      match_labels = { app = "demo" }
    }

    template {
      metadata { labels = { app = "demo" } }
      spec {
        container {
          name  = "demo"
          image = "nginx:1.27"
          port { container_port = 80 }
        }
      }
    }
  }
}

Die HCL-Syntax für K8s-Ressourcen ist verbose — das ist der Preis für Deklarativität ohne YAML. Für komplexe Manifeste mit vielen Feldern nutzen manche Teams yamlencode() oder den kubernetes_manifest-Resource-Typ (unterstützt beliebige API-Objekte inkl. CRDs).

Helm-Provider: Charts als Releases

Für Drittsoftware ist der Helm-Provider meist praktikabler als jedes Chart-Feld in HCL nachzubauen:

terraform {
  required_providers {
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.13"
    }
  }
}

provider "helm" {
  kubernetes {
    config_path = "~/.kube/config"
  }
}

resource "helm_release" "ingress_nginx" {
  name       = "ingress-nginx"
  repository = "https://kubernetes.github.io/ingress-nginx"
  chart      = "ingress-nginx"
  version    = "4.11.0"
  namespace  = kubernetes_namespace.platform.metadata[0].name

  values = [
    yamlencode({
      controller = {
        replicaCount = 2
        service = { type = "LoadBalancer" }
      }
    })
  ]
}

Terraform managed den Release-Zustand — Upgrades laufen über terraform plan, Rollback über Git-Revert + Apply, nicht über helm rollback. Das passt zum IaC-Workflow, widerspricht aber dem nativen Helm-Workflow mit Revisionen.

Wann Terraform, wann kubectl, wann GitOps?

AnsatzStärkeSchwäche
Terraform (K8s/Helm)Ein Plan für Cloud + Cluster; State-TrackingImperativ ausgeführt; kein kontinuierliches Reconcile
kubectl / Helm CLISchnell, direkt, Helm-RevisionsKein Review-Flow, kein Team-State
Argo CD / FluxKontinuierliche Reconciliation, Drift-FixZusätzliche Control Plane im Cluster

Typisches Plattform-Muster: Terraform legt Cluster, Basiskomponenten und Shared Services an (Ingress, Monitoring, CSI-Driver). Produktteams deployen darüber hinaus per GitOps oder CI — nicht alles durch den Platform-Terraform-Stack.

Terraform + Kubernetes ist imperativ wie reines Helm: Ändert jemand danach mit kubectl edit am Deployment vorbei, merkt Terraform das erst beim nächsten plan. GitOps schließt genau diese Lücke.

Provider-Kette: Cloud → Cluster → Helm

In Produktion hängt die Provider-Konfiguration oft von Cloud-Outputs ab:

data "aws_eks_cluster" "platform" {
  name = module.eks.cluster_name
}

data "aws_eks_cluster_auth" "platform" {
  name = module.eks.cluster_name
}

provider "kubernetes" {
  host                   = data.aws_eks_cluster.platform.endpoint
  cluster_ca_certificate = base64decode(data.aws_eks_cluster.platform.certificate_authority[0].data)
  token                  = data.aws_eks_cluster_auth.platform.token
}

provider "helm" {
  kubernetes {
    host                   = data.aws_eks_cluster.platform.endpoint
    cluster_ca_certificate = base64decode(data.aws_eks_cluster.platform.certificate_authority[0].data)
    token                  = data.aws_eks_cluster_auth.platform.token
  }
}

Der Abhängigkeitsgraph stellt sicher: Erst Cluster, dann K8s-Ressourcen, dann Helm-Releases. Ein apply ohne funktionierenden Cluster schlägt beim K8s-Provider fehl — deshalb splitten viele Teams Cloud- und Cluster-Stack (Lektion 6).

Praxis: Namespace und ConfigMap in kind

kind create cluster --name tf-k8s-lab
mkdir k8s-lab && cd k8s-lab
cat > main.tf <<'EOF'
terraform {
  required_providers {
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.30"
    }
  }
}

provider "kubernetes" {
  config_path = "~/.kube/config"
}

resource "kubernetes_namespace" "lab" {
  metadata { name = "tf-lab" }
}

resource "kubernetes_config_map" "config" {
  metadata {
    name      = "app-config"
    namespace = kubernetes_namespace.lab.metadata[0].name
  }
  data = { greeting = "Hallo aus Terraform" }
}
EOF

terraform init && terraform apply -auto-approve
kubectl get cm -n tf-lab
terraform destroy -auto-approve
kind delete cluster --name tf-k8s-lab

Typische Stolperfallen

Server-Side Apply vs. Terraform: Ab Kubernetes 1.18+ nutzt kubectl SSA; der Provider hat eigene Merge-Semantik. Felder, die der Cluster defaulted, können zu perpetual Drift im Plan führen — lifecycle { ignore_changes = [...] } als gezielter Workaround.

Helm-CRDs: Charts mit CRDs (Prometheus Operator, Crossplane) brauchen oft einen zweistufigen Apply oder crds-Handling — sonst schlägt der Release fehl, weil die API-Types fehlen.

Alles in einen Stack: Cloud + 50 Helm-Releases in einem State — jeder Plan dauert Minuten, jeder Fehler blockiert alles. Basiskomponenten in Terraform, App-Deployments woanders.

Terraform vs. GitOps doppelt: Dasselbe Deployment per Terraform und Argo CD managen erzeugt Kampf um Ownership. Klare Grenze ziehen.

Interview-Vorbereitung

„Wie nutzt du Terraform mit Kubernetes?” — K8s-Provider für einzelne Ressourcen, Helm-Provider für Charts; Provider-Konfiguration aus Cloud-Outputs; typisch für Cluster-Bootstrap und Shared Services.

Follow-ups:

  • „Terraform oder Argo CD?” — Terraform: imperative Apply-Zyklen, gut für Bootstrap; GitOps: kontinuierliches Reconcile, gut für App-Lifecycle.
  • „Was passiert bei kubectl edit an einer TF-managed Resource?” — Drift; nächster Plan will zurück zum Code-Zustand.
  • „Warum ist der Helm-Provider verbose?” — Values als HCL/yamlencode statt values.yaml — Trade-off für einen Plan über Cloud und Cluster.

Zusammenfassung

Der Kubernetes-Provider legt einzelne Cluster-Ressourcen deklarativ an; der Helm-Provider installiert Charts als Releases. Beide hängen typischerweise an Cloud-Outputs für die Authentifizierung. Terraform eignet sich für Cluster-Bootstrap und Shared Services — nicht als Ersatz für GitOps im App-Betrieb. Die imperativ ausgeführte Natur bleibt eine bewusste Grenze, die du im Interview benennen solltest.

In der nächsten Lektion automatisieren wir Plan und Apply in CI/CD-Pipelines.