GitOps en Terraform: waarom de combinatie krachtig is

GitOps transformeerde de implementatie van Kubernetes-applicaties: Git wordt de bron van de waarheid verzoent een controleur voortdurend de gewenste toestand met de werkelijke, elke wijziging gaat via een pull-request. In 2026 zal hetzelfde paradigma ingang vinden voor cloudinfrastructuur beheerd met Terraform, met één cruciaal verschil vergeleken met Traditionele CI/CD: in plaats van een push-and-forget-trigger heb je een verzoening doorgaan die afwijkingen automatisch detecteert en corrigeert.

Het probleem met traditionele Terraform-workflows gebaseerd op GitHub Actions of Atlantis e die ik ben reactief: Iemand brengt een handmatige wijziging aan op de AWS-console, en niemand het weet dit totdat de volgende pijpleiding loopt. Met GitOps voor Terraform kan elke discrepantie tussen de HCL-code en de werkelijke status wordt een waarschuwing – of wordt gecorrigeerd automatisch op basis van het geconfigureerde beleid.

Wat je gaat leren

  • GitOps-architectuur voor IaC: pull-model versus push-model
  • Flux Terraform Controller: installatie, Terraform-object CRD en afstemming
  • Terraform-statusbeheer van Kubernetes met S3- en IRSA-backend
  • Spacelift: stapels, Rego-beleid, RBAC en goedkeuringsworkflows
  • Driftdetectie: Slack/PagerDuty-waarschuwing voor ongeoorloofde afwijkingen
  • Patroon voor kritieke omgevingen: automatisch herstel versus handmatige goedkeuring

Pull-model versus push-model voor IaC

Het belangrijkste onderscheid tussen GitOps en traditionele CI/CD is het synchronisatiemodel. In de duwmodel (GitHub Actions, Jenkins), wordt de pijplijn bij elke commit geactiveerd en “pusht” veranderingen in de infrastructuur. In de trekmodel (puur GitOps), een agent die binnen het cluster draait, "haalt" continu de gewenste status uit de repository en verzoenen. Dit verschil heeft diepgaande gevolgen voor de veiligheid en veerkracht:

# Push Model (GitHub Actions) — richiede credenziali cloud nella pipeline
# Il runner GitHub deve avere accesso outbound al cloud provider
# Problem: se il job fallisce a meta, lo state puo essere inconsistente

# Pull Model (Flux TF Controller) — l'agente vive dentro il cluster
# Solo il cluster Kubernetes ha le credenziali cloud (via IRSA o Workload Identity)
# Vantaggio: single point of trust, nessuna credenziale nelle GitHub Secrets
# Vantaggio: riconciliazione continua ogni N minuti, non solo su commit

# Confronto security:
# Push Model: GitHub runner --[credenziali]--> AWS/Azure/GCP
# Pull Model: Kubernetes pod -[IRSA/WI]--> AWS/Azure/GCP
#             Git repository -[SSH/HTTPS]--> Flux controller (dentro cluster)

Flux Terraform-controller

Il Flux Terraform-controller (tf-controller) en een Kubernetes-controller open-source die Terraform in de GitOps-wereld brengt. Het is een Flux gemeenschapsproject (Weaveworks + onafhankelijke onderhouder) die Flux uitbreidt met de mogelijkheid om te draaien plannen en Terraform toepassen als native Kubernetes-afstemmingslussen.

Installatie

# Prerequisiti: cluster Kubernetes + Flux installato
# Installa Flux sul cluster (se non presente)
flux install

# Installa il TF Controller tramite HelmRelease
cat <<'EOF' | kubectl apply -f -
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
  name: tf-controller
  namespace: flux-system
spec:
  interval: 1h
  url: https://weaveworks.github.io/tf-controller
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: tf-controller
  namespace: flux-system
spec:
  interval: 1h
  chart:
    spec:
      chart: tf-controller
      version: "0.16.x"
      sourceRef:
        kind: HelmRepository
        name: tf-controller
        namespace: flux-system
  values:
    replicaCount: 1
    resources:
      limits:
        cpu: "1"
        memory: 1Gi
      requests:
        cpu: 200m
        memory: 512Mi
    # Runner pods: eseguono il processo terraform effettivo
    runner:
      image:
        tag: "v1.5.x-flux"
EOF

# Verifica installazione
kubectl get pods -n flux-system | grep tf-controller
# NAME                                          READY   STATUS    RESTARTS
# tf-controller-6d8f9b4b5-xn7q2               1/1     Running   0

GitRepository en Terraform CRD-configuratie

De workflow is gebaseerd op twee Kubernetes-objecten: a GitRepository waar naar verwijst repository met de HCL-code en een object Terraform (CRD aangepast) dat definieert wat te verzoenen.

# 1. GitRepository: sorgente del codice HCL
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: GitRepository
metadata:
  name: infra-repo
  namespace: flux-system
spec:
  interval: 1m          # Controlla il repo ogni minuto
  url: https://github.com/myorg/terraform-infra
  ref:
    branch: main
  secretRef:
    name: github-ssh-key  # Secret con chiave SSH o token

---
# 2. Terraform CRD: definisce il modulo da riconciliare
apiVersion: infra.contrib.fluxcd.io/v1alpha2
kind: Terraform
metadata:
  name: aws-networking
  namespace: flux-system
spec:
  # Intervallo di riconciliazione
  interval: 10m

  # Sorgente HCL
  sourceRef:
    kind: GitRepository
    name: infra-repo
  path: ./environments/prod/networking   # Path nel repo

  # Approvazione automatica (auto-apply) o manuale
  approvePlan: auto

  # Gestione del drift: se lo stato reale differisce dal desired
  # force: riconcilia automaticamente
  # drift: solo alert, non corregge
  enableInventory: true

  # Backend per lo state (S3 con IRSA)
  backendConfig:
    customConfiguration: |
      backend "s3" {
        bucket         = "myorg-terraform-state-prod"
        key            = "networking/terraform.tfstate"
        region         = "eu-west-1"
        dynamodb_table = "terraform-state-lock"
        encrypt        = true
      }

  # Variabili passate al modulo
  vars:
    - name: environment
      value: prod
    - name: aws_region
      value: eu-west-1

  # Variabili da Secret Kubernetes (per segreti)
  varsFrom:
    - kind: Secret
      name: terraform-vars-prod
      varsKeys:
        - db_password
        - api_key

IRSA voor AWS-toegang vanuit Kubernetes

Best practices voor AWS-authenticatie van Kubernetes e IRSA (IAM-rollen voor serviceaccounts): De Terraform-pod ontvangt een JWT-token ondertekend door de cluster dat wordt uitgewisseld met tijdelijke AWS-inloggegevens, zonder enige sleutel hardgecodeerd in het cluster.

# Crea il Service Account con annotazione IRSA
kubectl create serviceaccount tf-runner -n flux-system

kubectl annotate serviceaccount tf-runner \
  -n flux-system \
  eks.amazonaws.com/role-arn=arn:aws:iam::123456789:role/TerraformRunnerRole

# IAM Role Trust Policy (da configurare su AWS):
# {
#   "Version": "2012-10-17",
#   "Statement": [{
#     "Effect": "Allow",
#     "Principal": {
#       "Federated": "arn:aws:iam::123456789:oidc-provider/oidc.eks.eu-west-1.amazonaws.com/..."
#     },
#     "Action": "sts:AssumeRoleWithWebIdentity",
#     "Condition": {
#       "StringEquals": {
#         "oidc.eks.eu-west-1.amazonaws.com/...:sub":
#           "system:serviceaccount:flux-system:tf-runner"
#       }
#     }
#   }]
# }

# Aggiorna il CRD Terraform per usare il Service Account
# Aggiungi nella spec:
# serviceAccountName: tf-runner

Driftdetectie en meldingen

Drift ontstaat wanneer de werkelijke toestand van de infrastructuur daarvan afwijkt beschreven in de HCL-code – meestal voor handmatige wijzigingen op de cloudconsole. De TF-controller detecteert de afwijking bij elke afstemmingscyclus en rapporteert deze via hij Alert door Flux.

# Alert Flux per notifiche Slack sul drift
apiVersion: notification.toolkit.fluxcd.io/v1beta2
kind: Provider
metadata:
  name: slack-infra
  namespace: flux-system
spec:
  type: slack
  channel: "#infra-alerts"
  secretRef:
    name: slack-webhook-url

---
apiVersion: notification.toolkit.fluxcd.io/v1beta2
kind: Alert
metadata:
  name: terraform-drift-alert
  namespace: flux-system
spec:
  providerRef:
    name: slack-infra
  eventSeverity: warning
  eventSources:
    - kind: Terraform
      name: "*"   # Tutti gli oggetti Terraform
  # Invia alert per questi eventi:
  # - drift detected
  # - reconciliation failed
  # - plan pending approval
# Verificare lo stato di drift manualmente
kubectl get terraform -n flux-system
# NAME              READY   STATUS                          AGE
# aws-networking    True    Reconciliation succeeded        2h
# aws-database      False   Drift detected: 3 resources     15m

# Dettaglio del drift
kubectl describe terraform aws-database -n flux-system | grep -A 20 "Conditions:"
# Conditions:
#   Last Transition Time:  2026-03-20T10:30:00Z
#   Message:               Drift detected: aws_db_instance.main (tags changed),
#                           aws_security_group.db (ingress rule added manually)
#   Reason:                TerraformOutputsWritten
#   Status:                False
#   Type:                  Ready

Spacelift: GitOps Enterprise voor Terraform

Ruimtelift en een doordacht SaaS-platform (met zelf-gehoste optie). voor teams die Terraform gebruiken in bedrijfsomgevingen. In tegenstelling tot de TF-controller Spacelift leeft binnen het Kubernetes-cluster en biedt een uitgebreide gebruikersinterface en geavanceerd beleid erin geschreven Rego (dezelfde taal als OPA), granulaire RBAC e goedkeuringsworkflow met volledig audittraject.

Belangrijke ruimteliftconcepten

# Struttura Spacelift
# Stack = equivalente di un workspace Terraform
# Ogni stack ha:
# - Source: GitHub/GitLab repository + branch + path
# - Runner image: immagine Docker con Terraform + provider
# - Environment variables: variabili e segreti
# - Policies: regole Rego applicate a plan/apply
# - Contexts: set di variabili condivisibili tra stack

# Creare uno stack via Spacelift API (Terraform provider spacelift):
resource "spacelift_stack" "networking_prod" {
  name        = "networking-prod"
  repository  = "terraform-infra"
  branch      = "main"
  project_root = "environments/prod/networking"

  # Auto-deploy su push al branch
  autodeploy = false   # Per prod: richiede approvazione manuale

  # Terraform version
  terraform_version = "1.9.x"

  labels = ["team:platform", "env:prod", "tier:networking"]
}

resource "spacelift_context_attachment" "networking_prod" {
  context_id = spacelift_context.aws_prod.id
  stack_id   = spacelift_stack.networking_prod.id
  priority   = 1
}

Beleidsrego op het gebied van ruimtelift

Het Rego-beleid is het sterke punt van Spacelift: het stelt je in staat vangrails te definiëren complexen die op elk plan worden beoordeeld alvorens te beslissen of er goedkeuring wordt aangevraagd, blokkeren of automatisch toepassen. En eigenlijk een programmeerbare poort.

# policy: require-approval-for-destructive-changes.rego
# Richiede approvazione umana se il plan contiene distruzioni

package spacelift

# Nega auto-apply se ci sono risorse da distruggere
deny[sprintf("Destroy richiede approvazione: %s", [resource])] {
  change := input.terraform.resource_changes[_]
  change.change.actions[_] == "delete"
  resource := change.address
}

# Blocca completamente se piu di 5 risorse vengono distrutte
deny["Piu di 5 destroy in un singolo plan: richiede approvazione senior"] {
  destroy_count := count([c |
    c := input.terraform.resource_changes[_]
    c.change.actions[_] == "delete"
  ])
  destroy_count > 5
}

# Warn (non blocca) per modifiche ai security group
warn[sprintf("Security group modificato: %s", [resource])] {
  change := input.terraform.resource_changes[_]
  change.type == "aws_security_group"
  change.change.actions[_] != "no-op"
  resource := change.address
}
# policy: cost-control.rego
# Blocca istanze grandi in ambienti non-prod

package spacelift

expensive_instance_types := {
  "m5.4xlarge", "m5.8xlarge", "m5.16xlarge",
  "c5.4xlarge", "c5.9xlarge",
  "r5.4xlarge", "r5.8xlarge"
}

deny[msg] {
  # Leggi i tag dallo stack Spacelift
  not contains(input.spacelift.stack.labels[_], "env:prod")

  # Cerca istanze EC2 con instance_type costoso
  change := input.terraform.resource_changes[_]
  change.type == "aws_instance"
  instance_type := change.change.after.instance_type
  expensive_instance_types[instance_type]

  msg := sprintf(
    "Istanza %s di tipo %s non consentita in ambienti non-prod",
    [change.address, instance_type]
  )
}

Goedkeuring workflow Spacelift

# Spacelift approval workflow con notifiche Slack

# 1. Developer fa push al branch feature/add-rds
# 2. Spacelift crea automaticamente un preview run
# 3. La policy Rego valuta il plan: contiene 1 destroy (vecchio RDS)
# 4. Spacelift blocca l'auto-deploy e notifica Slack
#    "Run #abc123 richiede approvazione: destroy aws_db_instance.old_db"
# 5. Senior engineer esamina il plan su Spacelift UI
# 6. Approva cliccando "Confirm" oppure aggiunge commento e rifiuta
# 7. Spacelift esegue l'apply o notifica il developer del blocco

# Via Spacelift CLI (spacectl):
spacectl stack run list --id networking-prod
# ID        COMMIT    STATE           CREATED AT
# abc123    f3a8b91   PENDING_REVIEW  2026-03-20 10:30
# xyz789    a1c2d3e   FINISHED        2026-03-19 14:22

spacectl run confirm --run abc123 --stack networking-prod
# Run abc123 confirmed, applying...

Geavanceerde driftdetectie: waarschuwingen en automatisch herstel

Driftdetectie is niet voldoende als dit niet gepaard gaat met een duidelijke responsstrategie. Er zijn drie benaderingen, elk met hun eigen afwegingen:

# Approccio 1: Solo Alert (ambienti critici, audit trail necessario)
# Il drift viene rilevato e segnalato, ma non corretto automaticamente
# Uso: database di produzione, networking critico

# Approccio 2: Auto-Remediation per drift minore
# Modifiche ai tag, aggiornamenti di patch: correggi automaticamente
# Blocca e avvisa per modifiche strutturali

# Approccio 3: Full Auto-Apply (ambienti dev/staging)
# Qualsiasi drift viene corretto immediatamente dal controller

---
# Esempio Flux TF Controller: configurazione per approccio ibrido
apiVersion: infra.contrib.fluxcd.io/v1alpha2
kind: Terraform
metadata:
  name: aws-networking-prod
  namespace: flux-system
spec:
  interval: 5m
  approvePlan: "auto"    # "auto" per ambienti non critici

  # Plan runner: genera il piano ma NON lo applica
  # L'apply richiede un secondo passaggio (manuale o automatico)
  planOnly: false

  # Dopo quanti drift consecutivi inviare un alert critico
  # (configurato via Flux Alert con severita error)
  retryInterval: 1m
  timeout: 5m
# Script di scheduled drift check (alternativa leggera senza GitOps controller)
#!/bin/bash
# drift-check.sh — eseguito ogni ora via cron o GitHub Actions scheduled

set -euo pipefail

ENVIRONMENTS=("dev" "staging" "prod")
SLACK_WEBHOOK="${SLACK_DRIFT_WEBHOOK}"

for ENV in "${ENVIRONMENTS[@]}"; do
  cd "/infra/environments/${ENV}"

  # Inizializza senza output
  terraform init -reconfigure -input=false -no-color > /dev/null 2>&1

  # Esegui plan e cattura l'exit code
  # 0 = no changes, 1 = error, 2 = changes detected (drift)
  set +e
  terraform plan -detailed-exitcode -no-color -out=/tmp/plan-${ENV} 2>&1
  EXITCODE=$?
  set -e

  if [ $EXITCODE -eq 2 ]; then
    CHANGES=$(terraform show -no-color /tmp/plan-${ENV} | \
      grep -E "^\s+(#|~|\+|-)" | head -20)

    curl -s -X POST "$SLACK_WEBHOOK" \
      -H "Content-Type: application/json" \
      -d "{
        \"text\": \"*DRIFT DETECTED* in environment: ${ENV}\n\`\`\`${CHANGES}\`\`\`\"
      }"
    echo "Drift alert sent for ${ENV}"
  elif [ $EXITCODE -eq 0 ]; then
    echo "${ENV}: no drift detected"
  else
    echo "ERROR: terraform plan failed for ${ENV}" >&2
    exit 1
  fi
done

Vergelijking: TF-controller versus Spacelift versus Atlantis

Wanneer welk hulpmiddel gebruiken

  • Flux TF-controller: Team dat Flux/Argo al gebruikt voor Kubernetes, wil pure en open-source GitOps, beheert AWS-infrastructuur met IRSA. Zelfgehoste, gratis, gemiddelde leercurve.
  • Ruimtelift: Enterprise-team met complexe RBAC-vereisten, audit trail, goedkeuringsworkflow met meerdere goedkeurders, geavanceerd Rego-beleid. Betaalde SaaS, geweldige UX, kant-en-klare integraties (Slack, PagerDuty, Jira).
  • Atlantis: Een team dat in het PR-gebaseerde paradigma wil blijven zonder Puur GitOps. Plan/Toepassen direct bespreekbaar in de PR. Zelf gehost, gratis, heel volwassen. Het kent geen natuurlijke, voortdurende verzoening.
  • Terraform Cloud/Enterprise: Natuurlijke keuze als deze al in het ecosysteem aanwezig is HashiCorp, native Sentinel-beleidstaal, Vault-integratie. Zie artikel 10.

Best practices voor GitOps IaC in productie

# Repository structure per GitOps Terraform
terraform-infra/
├── modules/                    # Moduli riusabili (non riconciliati direttamente)
│   ├── networking/
│   ├── compute/
│   └── database/
├── environments/
│   ├── dev/
│   │   ├── networking/         # Stack separati per ogni layer
│   │   │   ├── main.tf
│   │   │   └── terraform.auto.tfvars
│   │   ├── compute/
│   │   └── database/
│   ├── staging/
│   └── prod/
│       ├── networking/         # Ogni ambiente ha il suo state isolato
│       ├── compute/
│       └── database/
├── flux/                       # Manifesti Flux per i CRD Terraform
│   ├── dev/
│   │   ├── networking-tf.yaml
│   │   └── compute-tf.yaml
│   └── prod/
│       ├── networking-tf.yaml  # approvePlan: "auto" o manuale
│       └── compute-tf.yaml
└── policies/                   # Policy Rego (se Spacelift)
    ├── require-approval.rego
    └── cost-control.rego

Antipatroon: verzoening te agressief

Set interval: 1m met approvePlan: auto op omgevingen productie en gevaarlijk: een verandering die nog niet in hoofdzaak is samengevoegd, zou kunnen zijn vóór de beoordeling toegepast. De gouden regel: hoe kritischer het milieu, hoe langer en het interval is strenger en het goedkeuringsproces. Gebruik in de productie interval van 30m+ en vereisen altijd handmatige goedkeuring voor structurele wijzigingen.

Conclusies en volgende stappen

GitOps voor Terraform vertegenwoordigt de volwassenheid van Infrastructure as Code: niet meer op triggers gebaseerde pijplijnen, maar continue afstemming, geen inloggegevens meer in pijplijnen maar de eigen identiteit van het cluster, niet langer 'wie die verandering heeft aangebracht', maar audittrails compleet in Git. De Flux TF-controller is de ideale keuze voor Kubernetes-native teams, terwijl Spacelift aan de bedrijfsvereisten voldoet met zijn Rego-beleidsengine.

De complete serie: Terraform en IaC

  • Artikel 01 - Terraform from Scratch: HCL, Provider en Plan-Apply-Destroy
  • Artikel 02 — Herbruikbare Terraform-modules ontwerpen
  • Artikel 03 — Terraform State: externe backend met S3/GCS
  • Artikel 04 — Terraform in CI/CD: GitHub-acties en Atlantis
  • Artikel 05 — IaC-testen: Terratest en Terraform Test
  • Artikel 06 — IaC-beveiliging: Checkov, Trivy en OPA
  • Artikel 07 — Terraform Multi-Cloud: AWS + Azure + GCP
  • Artikel 08 (dit) — GitOps voor Terraform: Flux TF-controller, Spacelift en driftdetectie
  • Artikel 09 – Terraform versus Pulumi versus OpenTofu: vergelijking 2026
  • Artikel 10 — Terraform Enterprise-patronen: werkruimte, Sentinel en teamschaling