GitOps per Terraform: Flux TF Controller, Spacelift e Drift Detection
Porta Terraform nel paradigma GitOps: Flux Terraform Controller per la riconciliazione continua dallo stato del repository, Spacelift per policy e RBAC avanzati e alert di drift per ambienti critici.
GitOps e Terraform: Perche la Combinazione e Potente
GitOps ha trasformato il deployment delle applicazioni Kubernetes: Git diventa la fonte di verita, un controller riconcilia continuamente lo stato desiderato con quello reale, ogni modifica passa per una pull request. Nel 2026, lo stesso paradigma si sta affermando per l'infrastruttura cloud gestita con Terraform, con una differenza cruciale rispetto al CI/CD tradizionale: invece di un trigger "push-and-forget", hai una riconciliazione continua che rileva e corregge i drift automaticamente.
Il problema con i workflow Terraform tradizionali basati su GitHub Actions o Atlantis e che sono reattivi: qualcuno fa una modifica manuale sulla console AWS, e nessuno lo sa finche non viene eseguita la prossima pipeline. Con GitOps per Terraform, ogni discrepanza tra il codice HCL e lo stato reale diventa un alert - o viene corretta automaticamente in base alla policy configurata.
Cosa Imparerai
- Architettura GitOps per IaC: pull model vs push model
- Flux Terraform Controller: installazione, Terraform object CRD e riconciliazione
- Gestione del state Terraform da Kubernetes con backend S3 e IRSA
- Spacelift: stacks, policies Rego, RBAC e approval workflows
- Drift detection: alert Slack/PagerDuty per deviazioni non autorizzate
- Pattern per ambienti critici: auto-remediation vs manual approval
Pull Model vs Push Model per IaC
La distinzione fondamentale tra GitOps e CI/CD tradizionale e il modello di sincronizzazione. Nel push model (GitHub Actions, Jenkins), la pipeline si attiva su ogni commit e "spinge" i cambiamenti verso l'infrastruttura. Nel pull model (GitOps puro), un agente che gira all'interno del cluster "tira" continuamente lo stato desiderato dal repository e riconcilia. Questa differenza ha implicazioni profonde per la sicurezza e la resilienza:
# 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) e un controller Kubernetes open-source che porta Terraform nel mondo GitOps. E un progetto della community Flux (Weaveworks + maintainer indipendenti) che estende Flux con la capacita di eseguire piani e apply Terraform come reconciliation loop nativi di Kubernetes.
Installazione
# 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
Configurazione GitRepository e Terraform CRD
Il workflow si basa su due oggetti Kubernetes: un GitRepository che punta al
repository con il codice HCL, e un oggetto Terraform (CRD custom) che definisce
cosa riconciliare.
# 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 per Accesso AWS da Kubernetes
La best practice per l'autenticazione AWS da Kubernetes e IRSA (IAM Roles for Service Accounts): il pod Terraform riceve un token JWT firmato dal cluster che viene scambiato con credenziali AWS temporanee, senza nessuna chiave hardcoded nel 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
Drift Detection e Notifiche
Il drift si verifica quando lo stato reale dell'infrastruttura differisce da quello
descritto nel codice HCL - solitamente per modifiche manuali sulla console cloud.
Il TF Controller rileva il drift a ogni ciclo di riconciliazione e lo notifica tramite
gli Alert di 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 per Terraform
Spacelift e una piattaforma SaaS (con opzione self-hosted) pensata per team che gestiscono Terraform in ambienti enterprise. A differenza del TF Controller che vive dentro il cluster Kubernetes, Spacelift offre una UI completa, policy avanzate scritte in Rego (lo stesso linguaggio di OPA), RBAC granulare e approval workflow con audit trail completo.
Concetti Chiave di Spacelift
# 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
}
Policy Rego in Spacelift
Le policy Rego sono il punto di forza di Spacelift: permettono di definire guardrail complessi che vengono valutati su ogni piano prima di decidere se richiedere approvazione, bloccare, o fare auto-apply. E fondamentalmente un gate programmabile.
# 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]
)
}
Approval 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...
Drift Detection Avanzata: Alert e Auto-Remediation
La drift detection non basta se non e accompagnata da una chiara strategia di risposta. Ci sono tre approcci, ognuno con i propri trade-off:
# 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
Confronto: TF Controller vs Spacelift vs Atlantis
Quando Usare Quale Strumento
- Flux TF Controller: Team che usa gia Flux/Argo per Kubernetes, vuole GitOps puro e open-source, gestisce infrastruttura AWS con IRSA. Self-hosted, gratis, curva di apprendimento media.
- Spacelift: Team enterprise con requisiti RBAC complessi, audit trail, approval workflow con piu approvatori, policy avanzate Rego. SaaS a pagamento, ottima UX, integrazioni out-of-the-box (Slack, PagerDuty, Jira).
- Atlantis: Team che vuole rimanere nel paradigma PR-based senza GitOps puro. Piano/Apply commentabili direttamente nella PR. Self-hosted, gratis, molto maturo. Non ha riconciliazione continua nativa.
- Terraform Cloud/Enterprise: Scelta naturale se gia nell'ecosistema HashiCorp, Sentinel policy language nativo, integrazione con Vault. Vedi Articolo 10.
Best Practices per GitOps IaC in Produzione
# 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
Anti-Pattern: Riconciliazione Troppo Aggressiva
Impostare interval: 1m con approvePlan: auto su ambienti
di produzione e pericoloso: una modifica non ancora mergiata in main potrebbe essere
applicata prima della review. La regola d'oro: piu l'ambiente e critico, piu lungo
e l'intervallo e piu stringente e il processo di approvazione. In prod, usa interval
di 30m+ e richiedi sempre approvazione manuale per le modifiche strutturali.
Conclusioni e Prossimi Passi
GitOps per Terraform rappresenta la maturita dell'Infrastructure as Code: non piu pipeline trigger-based ma riconciliazione continua, non piu credenziali nelle pipeline ma identita native del cluster, non piu "chi ha fatto quella modifica" ma audit trail completo in Git. Il Flux TF Controller e la scelta ideale per team Kubernetes-native, mentre Spacelift soddisfa i requisiti enterprise con la sua policy engine Rego.
La Serie Completa: Terraform e IaC
- Articolo 01 - Terraform da Zero: HCL, Provider e Plan-Apply-Destroy
- Articolo 02 - Progettare Moduli Terraform Riusabili
- Articolo 03 - Terraform State: Backend Remoto con S3/GCS
- Articolo 04 - Terraform in CI/CD: GitHub Actions e Atlantis
- Articolo 05 - Testing IaC: Terratest e Terraform Test
- Articolo 06 - Sicurezza IaC: Checkov, Trivy e OPA
- Articolo 07 - Terraform Multi-Cloud: AWS + Azure + GCP
- Articolo 08 (questo) - GitOps per Terraform: Flux TF Controller, Spacelift e Drift Detection
- Articolo 09 - Terraform vs Pulumi vs OpenTofu: Confronto 2026
- Articolo 10 - Terraform Enterprise Patterns: Workspace, Sentinel e Team Scaling

Commenti
Caricamento commenti...
Accedi per commentare