Terraform State: externe backend met S3/GCS, vergrendelen en importeren
Als er één aspect van Terraform is dat de meeste ongelukken in de productie veroorzaakt, dan is het wel
en staatsmanagement. Ik heb teams uren zien verspillen met het repareren van corrupte staten,
uiteenlopende configuraties omdat twee ontwikkelaars het deden apply parallel,
‘spookbronnen’ die in de cloud bestaan, maar niet in de staat. Al deze problemen
ze hebben een gemeenschappelijke oorzaak: state local zonder vergrendeling.
Deze gids is technisch en praktisch: we zullen een S3-backend configureren met DynamoDB-vergrendeling voor AWS,
de GCS backend voor Google Cloud, we zullen het hebben over werkruimten voor het scheiden van omgevingen
en we zullen zien hoe importeer bestaande bronnen zonder downtime te gebruiken
het blok import geïntroduceerd in Terraform 1.5 (eindelijk declaratief en veilig).
Wat je gaat leren
- Waarom de lokale status gevaarlijk is in teams en hoe je naar een externe backend kunt migreren
- S3-backend instellen met DynamoDB-vergrendeling op AWS (klaar voor productie)
- Configureer GCS-backend op Google Cloud Platform
- Werkruimte Terraform: patroon voor het scheiden van omgevingen
- Importeer bestaande assets met vergrendeling
import(Terravorm 1.5+) - Noodoperaties: staatsmanipulatie, gedwongen ontgrendeling, back-up
- Gedeeltelijke backend-configuratie: beheer inloggegevens zonder hardcoding
Waarom de externe backend fundamenteel is
Het bestand terraform.tfstate local heeft vier fundamentele problemen in een team:
-
Geen vergrendeling: als twee ontwikkelaars uitvoeren
terraform applytegelijkertijd lezen ze dezelfde status en schrijven ze gedeeltelijke wijzigingen, wat leidt tot een corrupte staat en dubbele hulpbronnen. - Niet gedeeld: elke ontwikkelaar heeft zijn eigen lokale kopie. Wie heeft de nieuwste versie? Wie heeft de laatste aanvraag ingediend? Onmogelijk om te weten.
- Bevat geheimen: de status bevat vaak gevoelige waarden (RDS-wachtwoord, API-sleutels). Het komt nooit in Git terecht.
- Geen geschiedenis: er is geen audittrail van wie wat en wanneer heeft gedaan.
De externe backend lost al deze problemen op: gecentraliseerde status, atomaire vergrendeling, versleuteling in rust en versiebeheer voor terugdraaien.
Bootstrap: backend-bronnen creëren met Terraform
De paradox van de Terraform-backend is dat je AWS-bronnen moet creëren (S3-bucket + DynamoDB-tabel) Voor om de backend te kunnen configureren. De oplossing is om op te starten met een miniproject afzonderlijk dat gebruikmaakt van de lokale backend.
# 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"
}
Configureer de S3-backend
Nadat u uw bootstrapresources hebt gemaakt, stelt u de backend in uw projecten in.
Het blok backend ondersteunt geen Terraform-variabelen (en een bekende beperking),
dus gebruik de gedeeltelijke backend-configuratie om referenties te scheiden.
# 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
Het DynamoDB-slot
Wanneer terraform apply en actief, er wordt een rij gemaakt in DynamoDB
met LockID = {bucket}/{key}. Als het proces abrupt wordt gestopt
(kill, crash), wordt het slot mogelijk niet ontgrendeld. Ontgrendelen geforceerd:
terraform force-unlock {LOCK_ID}
# Il LOCK_ID e visibile nel messaggio di errore quando Terraform trova il lock
VS force-unlock alleen als je het zeker weet dat er niemand anders is
aanvraag in behandeling. Als u de blokkering opheft terwijl een toepassing bezig is, kan de status beschadigd raken.
GCS-backend voor 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"
]
}
Werkruimte Terraform: scheiding van omgevingen
Met Terraform-werkruimten kunt u afzonderlijke statusbestanden onderhouden
dezelfde HCL-configuratie. Elke werkruimte heeft zijn eigen statusbestand in de 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
# ...
}
Werkruimte versus afzonderlijke mappen: wanneer moet u wat gebruiken?
Werkruimtes zijn geschikt voor omgevingen structureel identiek die verschillen alleen voor schalen (dev/staging/prod). Als de omgevingen verschillende architecturen hebben (Prod heeft bijvoorbeeld een VPN, dev niet), gebruik aparte mappen die modules hergebruiken. Veel teams met complexe infrastructuren geven altijd de voorkeur aan afzonderlijke mappen voor maximale duidelijkheid.
Importeer bestaande bronnen met het importblok
Een van de meest voorkomende gevallen bij het adopteren van Terraform in een bestaande organisatie is het moeten doen
beheer bronnen die handmatig of met andere tools zijn gemaakt. Vóór Terraform 1.5 was import
alleen imperatief (terraform import CLI), riskant en niet-verifieerbaar.
De blok import (GA in 1,5, verbeterd in 1,7 met
import generate) en verklarend en zelfverzekerd.
# 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)
Staatsmanipulatie: noodoperaties
Er zijn situaties waarin het nodig is de staat rechtstreeks te manipuleren. Deze handelingen zijn krachtig en riskant: voer ze altijd uit met een preventieve back-up.
# 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ën voor grote teams
In organisaties met grote infrastructuren wordt het hebben van één enkele monolithische staat
een probleem: elke plan moet honderden bronnen controleren, sloten blokkeren
het hele team, en een fout in één module blokkeert de hele implementatie. De oplossing is verdelen
jij blijft erin onafhankelijke stapels.
# 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-beleid voor de S3-backend
In productie moet de door Terraform gebruikte IAM-rol (ontwikkelaars of CI/CD) aanwezig zijn de minimale machtigingen die nodig zijn voor de backend. Hier is het volledige beleid:
{
"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/*"
}
]
}
Conclusies en volgende stappen
Staatsmanagement is de basis waarop u al het andere in uw opstelling bouwt Terraform. Een externe backend met goede vergrendeling beschermt u tegen een hele klas van operationele ongevallen. Neem de tijd om het vanaf het begin correct in te stellen: migreren van de lokale staat naar afgelegen op een bestaand en haalbaar maar moeizaam project.
De volgende stap is het integreren van Terraform in een professionele CI/CD-pijplijn:
niet zomaar uitvoeren plan e apply automatisch, maar beheer
de planbeoordelingsworkflow in Pull Requests, het blokkeren van niet-geautoriseerde applicaties
en melding van gedetecteerde afwijkingen.
De complete serie: Terraform en IaC
- Artikel 01 - Terraform from Scratch: HCL, Provider en Plan-Apply-Destroy
- Artikel 02 — Herbruikbare Terraform-modules ontwerpen: structuur, I/O en register
- Artikel 03 (dit) — Terraform State: externe backend met S3/GCS, vergrendelen en importeren
- Artikel 04 — Terraform in CI/CD: GitHub-acties, Atlantis en Pull Request Workflow
- Artikel 05 — IaC-testen: Terratest, Terraform Native Test en Contracttesten
- Artikel 06 — IaC-beveiliging: Checkov, Trivy en OPA Policy-as-Code
- Artikel 07 — Terraform Multi-Cloud: AWS + Azure + GCP met gedeelde modules
- Artikel 08 — GitOps voor Terraform: Flux TF-controller, Spacelift en driftdetectie
- Artikel 09 — Terraform versus Pulumi versus OpenTofu: definitieve vergelijking 2026
- Artikel 10 — Terraform Enterprise-patronen: werkruimte, Sentinel en teamschaling







