Terraform State: Backend Remoto con S3/GCS, Locking e Import
Se c'e un unico aspetto di Terraform che causa piu incidenti in produzione,
e la gestione dello state. Ho visto team perdere ore a risolvere state corrotto,
configurazioni divergenti perche due sviluppatori hanno fatto apply in parallelo,
risorse "fantasma" che esistono nel cloud ma non nello state. Tutti questi problemi
hanno una causa comune: state locale senza locking.
Questa guida e tecnica e pratica: configureremo un backend S3 con DynamoDB locking per AWS,
il backend GCS per Google Cloud, parleremo di workspace per la separazione degli ambienti
e vedremo come importare risorse esistenti senza downtime usando
il blocco import introdotto in Terraform 1.5 (finalmente dichiarativo e sicuro).
Cosa Imparerai
- Perche lo state locale e pericoloso in team e come migrare al remote backend
- Configurare S3 backend con DynamoDB locking su AWS (produzione-ready)
- Configurare GCS backend su Google Cloud Platform
- Workspace Terraform: pattern per la separazione degli ambienti
- Importare risorse esistenti con il blocco
import(Terraform 1.5+) - Operazioni di emergenza: state manipulation, unlock forzato, backup
- Partial backend configuration: gestire le credenziali senza hardcoding
Perche il Backend Remoto e Fondamentale
Il file terraform.tfstate locale ha quattro problemi fondamentali in un team:
-
Nessun locking: se due sviluppatori eseguono
terraform applycontemporaneamente, entrambi leggono lo stesso state e scrivono modifiche parziali, portando a state corrotto e risorse duplicate. - Non e condiviso: ogni sviluppatore ha la sua copia locale. Chi ha l'ultima versione? Chi ha fatto l'ultimo apply? Impossibile saperlo.
- Contiene segreti: lo state spesso include valori sensibili (password RDS, chiavi API). Non va mai in Git.
- Nessuna history: non c'e audit trail di chi ha fatto cosa e quando.
Il backend remoto risolve tutti questi problemi: stato centralizzato, locking atomico, encryption at rest e versioning per rollback.
Bootstrap: Creare le Risorse per il Backend con Terraform
Il paradosso del backend Terraform e che devi creare le risorse AWS (S3 bucket + DynamoDB table) prima di poter configurare il backend. La soluzione e bootstrappare con un mini-progetto separato che usa il backend locale.
# bootstrap/main.tf - crea le risorse per il backend remoto
# Questo progetto usa il backend locale (terraform.tfstate nella directory)
# Committalo nel repo come "infra/bootstrap/"
terraform {
required_version = ">= 1.8.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# NOTA: qui non c'e backend block - usa il file locale
}
provider "aws" {
region = "eu-west-1"
}
locals {
name = "acme"
environment = "global"
}
# S3 Bucket per lo state
resource "aws_s3_bucket" "terraform_state" {
bucket = "${local.name}-terraform-state"
# Protezione contro cancellazione accidentale
lifecycle {
prevent_destroy = true
}
tags = {
Name = "${local.name}-terraform-state"
ManagedBy = "Terraform"
Environment = local.environment
}
}
# Versioning: ogni apply crea una nuova versione del file di state
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
# Encryption at rest: obbligatorio per state con segreti
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
bucket_key_enabled = true # Riduce i costi KMS del 99%
}
}
# Blocca accesso pubblico: lo state NON deve essere pubblico
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Policy del bucket: nega qualsiasi accesso non-TLS
resource "aws_s3_bucket_policy" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "DenyNonTLS"
Effect = "Deny"
Principal = "*"
Action = "s3:*"
Resource = [
aws_s3_bucket.terraform_state.arn,
"${aws_s3_bucket.terraform_state.arn}/*"
]
Condition = {
Bool = {
"aws:SecureTransport" = "false"
}
}
}
]
})
}
# DynamoDB table per il locking
resource "aws_dynamodb_table" "terraform_locks" {
name = "${local.name}-terraform-locks"
billing_mode = "PAY_PER_REQUEST" # Nessun costo fisso
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
# Protezione contro cancellazione
lifecycle {
prevent_destroy = true
}
tags = {
Name = "${local.name}-terraform-locks"
ManagedBy = "Terraform"
Environment = local.environment
}
}
# Output: valori da copiare nel backend block dei progetti
output "state_bucket_name" {
value = aws_s3_bucket.terraform_state.id
description = "Nome del bucket S3 per lo state Terraform"
}
output "dynamodb_table_name" {
value = aws_dynamodb_table.terraform_locks.name
description = "Nome della tabella DynamoDB per il locking"
}
output "aws_region" {
value = "eu-west-1"
description = "Region AWS dove sono create le risorse del backend"
}
Configurare il Backend S3
Una volta create le risorse bootstrap, configura il backend nei tuoi progetti.
Il blocco backend non supporta variabili Terraform (e una limitazione nota),
quindi usa la partial backend configuration per separare le credenziali.
# versions.tf - configurazione backend S3
terraform {
required_version = ">= 1.8.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# Backend con partial configuration (valori statici non-sensibili)
backend "s3" {
bucket = "acme-terraform-state"
key = "environments/prod/networking/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "acme-terraform-locks"
encrypt = true
# NON mettere access_key e secret_key qui!
# Usa variabili di ambiente o IAM Role
}
}
# La struttura raccomandata per il "key" (path del file):
# {account}/{environment}/{stack}/terraform.tfstate
#
# Esempi:
# "123456789/dev/networking/terraform.tfstate"
# "123456789/prod/eks-cluster/terraform.tfstate"
# "123456789/shared/monitoring/terraform.tfstate"
# Inizializzazione con partial backend config
# (passa i valori mancanti come -backend-config)
terraform init \
-backend-config="bucket=acme-terraform-state" \
-backend-config="key=environments/dev/networking/terraform.tfstate" \
-backend-config="region=eu-west-1" \
-backend-config="dynamodb_table=acme-terraform-locks"
# Oppure con file di backend config
# backend.hcl (NON committare se contiene credenziali)
# bucket = "acme-terraform-state"
# key = "environments/dev/networking/terraform.tfstate"
# region = "eu-west-1"
# dynamodb_table = "acme-terraform-locks"
terraform init -backend-config=backend.hcl
# Migra da backend locale a remoto
terraform init -migrate-state
# Terraform chiede conferma prima di copiare il state locale su S3
Il Lock DynamoDB
Quando terraform apply e in esecuzione, viene creata una riga in DynamoDB
con LockID = {bucket}/{key}. Se il processo viene interrotto bruscamente
(kill, crash), il lock potrebbe non venire rilasciato. Per forzare l'unlock:
terraform force-unlock {LOCK_ID}
# Il LOCK_ID e visibile nel messaggio di errore quando Terraform trova il lock
Usa force-unlock solo se sei certo che non c'e nessun altro
apply in corso. Sbloccare mentre un apply e in corso puo corrompere lo state.
Backend GCS per Google Cloud Platform
# versions.tf - backend GCS
terraform {
required_version = ">= 1.8.0"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
backend "gcs" {
bucket = "acme-terraform-state-eu"
prefix = "environments/prod/networking"
# prefix = "environments/{environment}/{stack}"
# Il file effettivo sara: {prefix}/default.tfstate
}
}
# Crea il bucket GCS per lo state (bootstrap)
resource "google_storage_bucket" "terraform_state" {
name = "acme-terraform-state-eu"
location = "EU"
force_destroy = false
# Versioning per rollback
versioning {
enabled = true
}
# Encryption con CMEK (opzionale ma consigliato)
# encryption {
# default_kms_key_name = google_kms_crypto_key.terraform_state.id
# }
# Policy di retention: mantieni le versioni per 30 giorni
lifecycle_rule {
condition {
num_newer_versions = 5
with_state = "ARCHIVED"
}
action {
type = "Delete"
}
}
}
# Blocca l'accesso pubblico al bucket
resource "google_storage_bucket_iam_binding" "terraform_state_private" {
bucket = google_storage_bucket.terraform_state.name
role = "roles/storage.admin"
members = [
"serviceAccount:terraform@acme-project.iam.gserviceaccount.com"
]
}
Workspace Terraform: Separazione degli Ambienti
I workspace Terraform permettono di mantenere state file separati a partire dalla
stessa configurazione HCL. Ogni workspace ha il suo file di state nel backend:
env:/{workspace_name}/terraform.tfstate.
# Gestione dei workspace
terraform workspace list
# * default
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace list
# default
# * dev
# staging
# prod
terraform workspace select prod
terraform workspace show # stampa il nome del workspace corrente
# Cancella un workspace (deve essere vuoto)
terraform workspace delete staging
# Uso del workspace nella configurazione HCL
locals {
# Recupera il nome del workspace corrente
workspace = terraform.workspace
# Configurazioni specifiche per workspace
workspace_config = {
dev = {
instance_type = "t3.micro"
min_instances = 1
max_instances = 2
enable_monitoring = false
rds_class = "db.t3.micro"
multi_az = false
}
staging = {
instance_type = "t3.small"
min_instances = 1
max_instances = 3
enable_monitoring = true
rds_class = "db.t3.small"
multi_az = false
}
prod = {
instance_type = "t3.medium"
min_instances = 2
max_instances = 6
enable_monitoring = true
rds_class = "db.t3.medium"
multi_az = true
}
}
# Accede alla configurazione del workspace corrente
# con fallback a "dev" se il workspace non e nella mappa
current_config = lookup(local.workspace_config, local.workspace, local.workspace_config["dev"])
# Naming con workspace nel prefisso
name_prefix = "${local.workspace}-${var.project_name}"
}
# Usa i valori dalla configurazione del workspace
resource "aws_autoscaling_group" "web" {
min_size = local.current_config.min_instances
max_size = local.current_config.max_instances
desired_capacity = local.current_config.min_instances
# ...
}
resource "aws_db_instance" "main" {
instance_class = local.current_config.rds_class
multi_az = local.current_config.multi_az
# ...
}
Workspace vs Directory Separate: Quando Usare Cosa
I workspace sono adatti per ambienti strutturalmente identici che differiscono solo per il dimensionamento (dev/staging/prod). Se gli ambienti hanno architetture diverse (es. prod ha una VPN, dev no), usa directory separate che riusano moduli. Molti team con infrastrutture complesse preferiscono sempre le directory separate per la massima chiarezza.
Importare Risorse Esistenti con il Blocco import
Uno dei casi piu comuni quando si adotta Terraform in un'organizzazione esistente e dover
gestire risorse create manualmente o con altri tool. Prima di Terraform 1.5, l'import era
solo imperativo (terraform import CLI), rischioso e non verificabile.
Il blocco import (GA in 1.5, migliorato in 1.7 con
import generate) e dichiarativo e sicuro.
# main.tf - import dichiarativo (Terraform 1.5+)
# Step 1: definisci il blocco import
import {
# ID della risorsa nel cloud provider
id = "vpc-0123456789abcdef0"
# Punta alla risorsa HCL che vuoi associare
to = aws_vpc.main
}
# Step 2: scrivi la configurazione HCL della risorsa
# (puoi usare "terraform plan -generate-config-out=generated.tf" per auto-generarla)
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "prod-main-vpc"
Environment = "prod"
ManagedBy = "Terraform" # Aggiunta da noi
}
}
# Step 3: terraform plan verifica che lo state da importare
# corrisponda alla configurazione HCL.
# Se ci sono differenze, Terraform le evidenzia come "~ to change"
# Import multiplo: importa un intero gruppo di risorse
import {
id = "subnet-0abc123"
to = aws_subnet.public[0]
}
import {
id = "subnet-0def456"
to = aws_subnet.public[1]
}
# Import con for_each (Terraform 1.7+)
locals {
existing_subnets = {
"public-1" = "subnet-0abc123"
"public-2" = "subnet-0def456"
}
}
import {
for_each = local.existing_subnets
id = each.value
to = aws_subnet.public[each.key]
}
# Workflow completo di import
# 1. Genera automaticamente la configurazione HCL dalla risorsa esistente
# (Terraform 1.5+ con -generate-config-out)
terraform plan -generate-config-out=generated_imports.tf
# 2. Rivedi il file generato: contiene la configurazione della risorsa
# come Terraform la vede nel cloud provider
cat generated_imports.tf
# 3. Copia e adatta la configurazione nel tuo main.tf
# (il file generated non e perfetto, va revisionato)
# 4. Esegui plan per verificare che l'import sia corretto
terraform plan
# Se il plan mostra "Plan: 0 to add, 0 to change, 0 to destroy"
# con note "Will import", l'import e perfetto
# 5. Apply: esegue l'import effettivo e aggiorna lo state
terraform apply
# 6. Rimuovi i blocchi import dal codice dopo l'apply
# (non sono piu necessari una volta che le risorse sono nello state)
Manipolazione dello State: Operazioni di Emergenza
Esistono situazioni in cui e necessario manipolare lo state direttamente. Queste operazioni sono potenti e rischiose: eseguile sempre con un backup preventivo.
# SEMPRE fare backup prima di operazioni sullo state
terraform state pull > backup-$(date +%Y%m%d-%H%M%S).tfstate
# Rimuove una risorsa dallo state (la risorsa rimane nel cloud)
# Uso: quando vuoi che Terraform "dimentichi" una risorsa
# senza cancellarla (es. passi a gestirla con un altro tool)
terraform state rm aws_instance.legacy_server
# Sposta una risorsa da un indirizzo a un altro nello state
# Uso: refactoring del codice senza distruggere le risorse
terraform state mv aws_instance.web aws_instance.web_new
terraform state mv 'aws_subnet.public[0]' 'aws_subnet.public["eu-west-1a"]'
# Mostra i dettagli JSON di una risorsa nello state
terraform state show aws_vpc.main
# Pull dello state remoto in locale (utile per ispezione)
terraform state pull > current.tfstate
# Push di uno state locale sul backend remoto
# PERICOLOSO: sovrascrive lo state remoto senza conferma
# Usare solo per recovery da state corrotto
terraform state push recovered.tfstate
# Refresh: aggiorna lo state leggendo lo stato reale del cloud
# (ora deprecato a favore di terraform apply -refresh-only)
terraform apply -refresh-only
State Partitioning: Strategie per Team Grandi
In organizzazioni con infrastrutture grandi, avere un unico state monolitico diventa
un problema: ogni plan deve controllare centinaia di risorse, i lock bloccano
tutto il team, e un errore in un modulo blocca l'intero deploy. La soluzione e dividere
lo state in stack indipendenti.
# Struttura raccomandata per infrastrutture large-scale
# Ogni directory e un progetto Terraform indipendente con il suo state
environments/
├── prod/
│ ├── networking/ # State: prod/networking/terraform.tfstate
│ │ ├── main.tf
│ │ └── versions.tf
│ ├── eks-cluster/ # State: prod/eks-cluster/terraform.tfstate
│ │ ├── main.tf
│ │ └── versions.tf
│ ├── databases/ # State: prod/databases/terraform.tfstate
│ │ ├── main.tf
│ │ └── versions.tf
│ └── monitoring/ # State: prod/monitoring/terraform.tfstate
│ ├── main.tf
│ └── versions.tf
└── dev/
└── ...
# Per passare informazioni tra stack: terraform_remote_state data source
# stack eks-cluster recupera gli output da networking
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "acme-terraform-state"
key = "environments/prod/networking/terraform.tfstate"
region = "eu-west-1"
}
}
resource "aws_eks_cluster" "main" {
name = "prod-cluster"
role_arn = aws_iam_role.eks.arn
vpc_config {
# Usa gli output dallo stack networking
subnet_ids = data.terraform_remote_state.networking.outputs.private_subnet_ids
endpoint_private_access = true
endpoint_public_access = false
}
}
# Alternativa moderna: usare variabili con file .tfvars invece di
# terraform_remote_state. Piu sicuro (evita dipendenze tra state),
# meno conveniente (richiede aggiornamento manuale dei valori)
IAM Policy per il Backend S3
In produzione, il ruolo IAM usato da Terraform (sviluppatori o CI/CD) deve avere i permessi minimi necessari per il backend. Ecco la policy completa:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TerraformStateBucketAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket",
"s3:GetBucketVersioning",
"s3:GetEncryptionConfiguration"
],
"Resource": [
"arn:aws:s3:::acme-terraform-state",
"arn:aws:s3:::acme-terraform-state/*"
]
},
{
"Sid": "TerraformStateLocking",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem",
"dynamodb:DescribeTable"
],
"Resource": "arn:aws:dynamodb:eu-west-1:*:table/acme-terraform-locks"
},
{
"Sid": "TerraformKMSDecryptState",
"Effect": "Allow",
"Action": [
"kms:GenerateDataKey",
"kms:Decrypt"
],
"Resource": "arn:aws:kms:eu-west-1:*:key/*"
}
]
}
Conclusioni e Prossimi Passi
La gestione dello state e il fondamento su cui costruisci tutto il resto del tuo setup Terraform. Un backend remoto con locking corretto ti protegge da una classe intera di incidenti operativi. Prenditi il tempo di configurarlo correttamente fin dall'inizio: migrare da state locale a remoto su un progetto esistente e fattibile ma laborioso.
Il passo successivo e integrare Terraform in una pipeline CI/CD professionale:
non solo eseguire plan e apply automaticamente, ma gestire
il workflow di revisione dei plan nei Pull Request, il blocco degli apply non autorizzati
e la notifica dei drift rilevati.
La Serie Completa: Terraform e IaC
- Articolo 01 - Terraform da Zero: HCL, Provider e Plan-Apply-Destroy
- Articolo 02 - Progettare Moduli Terraform Riusabili: Struttura, I/O e Registry
- Articolo 03 (questo) - Terraform State: Backend Remoto con S3/GCS, Locking e Import
- Articolo 04 - Terraform in CI/CD: GitHub Actions, Atlantis e Pull Request Workflow
- Articolo 05 - Testing IaC: Terratest, Terraform Test Nativo e Contract Testing
- Articolo 06 - Sicurezza IaC: Checkov, Trivy e OPA Policy-as-Code
- Articolo 07 - Terraform Multi-Cloud: AWS + Azure + GCP con Moduli Condivisi
- Articolo 08 - GitOps per Terraform: Flux TF Controller, Spacelift e Drift Detection
- Articolo 09 - Terraform vs Pulumi vs OpenTofu: Confronto Definitivo 2026
- Articolo 10 - Terraform Enterprise Patterns: Workspace, Sentinel e Team Scaling

Commenti
Caricamento commenti...
Accedi per commentare