Terraform State: Vzdálený backend s S3/GCS, zamykání a import
Pokud existuje jediný aspekt Terraformu, který způsobuje nejvíce nehod ve výrobě, je to
a řízení státu. Viděl jsem týmy ztrácet hodiny opravováním zkorumpovaných stavů,
odlišné konfigurace, protože to udělali dva vývojáři apply paralelně,
„duchové“ zdroje, které existují v cloudu, ale ne ve stavu. Všechny tyto problémy
mají společnou příčinu: stav místní bez zamykání.
Tato příručka je technická a praktická: nakonfigurujeme backend S3 se zamykáním DynamoDB pro AWS,
backend GCS pro Google Cloud, budeme hovořit o pracovních prostorech pro oddělení prostředí
a uvidíme jak importovat stávající zdroje bez použití prostojů
bloku import představeno v Terraformu 1.5 (konečně deklarativní a bezpečné).
Co se naučíte
- Proč je místní stát nebezpečný v týmech a jak migrovat na vzdálený backend
- Nastavení backendu S3 se zamykáním DynamoDB na AWS (připraveno na produkci)
- Nakonfigurujte backend GCS na platformě Google Cloud Platform
- Pracovní prostor Terraform: vzor pro oddělení prostředí
- Importujte existující aktiva s uzamčením
import(Teraform 1.5+) - Nouzové operace: manipulace se stavy, nucené odblokování, zálohování
- Částečná konfigurace backendu: spravujte přihlašovací údaje bez pevného kódování
Proč je vzdálený backend zásadní
Soubor terraform.tfstate local má v týmu čtyři základní problémy:
-
Bez zamykání: pokud provedou dva vývojáři
terraform applyzároveň oba čtou stejný stav a zapisují dílčí změny, vedoucí ke zkorumpovanému stavu a duplicitním zdrojům. - Nesdíleno: každý vývojář má svou vlastní lokální kopii. Kdo má nejnovější verze? Kdo podal poslední žádost? Nemožné vědět.
- Obsahuje tajemství: stav často obsahuje citlivé hodnoty (heslo RDS, API klíče). Nikdy to nejde do Gitu.
- Žádná historie: neexistuje žádná auditní stopa toho, kdo co a kdy udělal.
Vzdálený backend řeší všechny tyto problémy: centralizovaný stav, atomové zamykání, šifrování v klidu a verzování pro vrácení zpět.
Bootstrap: Vytváření backendových zdrojů s Terraformem
Paradoxem Terraform backendu je, že musíte vytvořit zdroje AWS (segment S3 + tabulka DynamoDB) Před abyste mohli nakonfigurovat backend. Řešením je bootstrap s miniprojektem samostatný, který používá místní 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"
}
Nakonfigurujte S3 Backend
Jakmile vytvoříte zdroje bootstrap, nastavte backend ve svých projektech.
Blok backend nepodporuje proměnné Terraform (a známé omezení),
tak použijte částečná konfigurace backendu oddělit pověření.
# 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
Zámek DynamoDB
Když terraform apply a běží, v DynamoDB se vytvoří řádek
s LockID = {bucket}/{key}. Pokud je proces náhle zastaven
(zabití, havárie), zámek nemusí být uvolněn. Vynucení odemknutí:
terraform force-unlock {LOCK_ID}
# Il LOCK_ID e visibile nel messaggio di errore quando Terraform trova il lock
USA force-unlock pouze pokud jste si jisti že nikdo jiný není
žádost probíhá. Odblokování v průběhu aplikace může poškodit stav.
Backend GCS pro 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"
]
}
Pracovní prostor Terraform: Oddělení prostředí
Pracovní prostory Terraform vám umožňují udržovat samostatné soubory stavu
stejná konfigurace HCL. Každý pracovní prostor má svůj vlastní stavový soubor v backendu:
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
# ...
}
Pracovní prostor vs samostatné adresáře: Kdy co použít
Pracovní prostory jsou vhodné pro prostředí konstrukčně identické které se liší pouze pro škálování (dev/staging/prod). Pokud mají prostředí různé architektury (např. produkt má VPN, vývojář ne), použijte samostatné adresáře kteří znovu používají moduly. Mnoho týmů se složitou infrastrukturou vždy preferuje samostatné adresáře pro maximální přehlednost.
Importujte existující zdroje pomocí importního bloku
Jedním z nejběžnějších případů, kdy je třeba přijmout Terraform ve stávající organizaci, je nutnost
spravovat zdroje vytvořené ručně nebo pomocí jiných nástrojů. Před Terraformem 1.5 byl import
pouze imperativ (terraform import CLI), rizikové a neověřitelné.
The blok import (GA v 1,5, zlepšeno o 1,7 s
import generate) a deklarativní a sebevědomý.
# 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)
Manipulace se stavem: nouzové operace
Jsou situace, kdy je nutné manipulovat stát přímo. Tyto operace jsou výkonné a riskantní: vždy je provádějte s preventivní zálohou.
# 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
Rozdělení státu: Strategie pro velké týmy
V organizacích s velkou infrastrukturou se stává jediným monolitickým státem
problém: každý plan musí ovládat stovky zdrojů, blokovat zámky
celý tým a chyba v jednom modulu zablokuje celé nasazení. Řešením je rozdělení
zůstaneš v něm nezávislé zásobníky.
# 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)
Zásady IAM pro backend S3
V produkci musí mít roli IAM používaná Terraformem (vývojáři nebo CI/CD). minimální oprávnění potřebná pro backend. Zde jsou úplné zásady:
{
"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/*"
}
]
}
Závěry a další kroky
State management je základem, na kterém stavíte vše ostatní ve vašem nastavení Terraform. Vzdálený backend se správným zamykáním vás chrání před celou třídou provozních nehod. Udělejte si čas na správné nastavení hned od začátku: migrovat ze státního místního do vzdáleného na existujícím a proveditelném, ale pracném projektu.
Dalším krokem je integrace Terraformu do profesionálního CI/CD potrubí:
nejen popravovat plan e apply automaticky, ale řídit
workflow kontroly plánu v Pull Requests, blokování neautorizovaných aplikací
a upozornění na zjištěné posuny.
Kompletní série: Terraform a IaC
- článek 01 — Terraform from Scratch: HCL, Provider a Plan-Apply-Destroy
- článek 02 — Návrh opakovaně použitelných modulů Terraform: struktura, I/O a registr
- článek 03 (tento) — Terraform State: Vzdálený backend s S3/GCS, zamykání a import
- Článek 04 — Terraform v CI/CD: GitHub Actions, Atlantis a Pull Request Workflow
- Článek 05 – Testování IaC: Terratest, Terraform Native Test a Smluvní testování
- Článek 06 – Zabezpečení IaC: Checkov, Trivy a OPA Policy-as-Code
- Článek 07 – Terraform Multi-Cloud: AWS + Azure + GCP se sdílenými moduly
- Článek 08 — GitOps pro Terraform: Flux TF Controller, Spacelift and Drift Detection
- Článek 09 — Terraform vs Pulumi vs OpenTofu: Konečné srovnání 2026
- Článek 10 — Terraform Enterprise Patterns: Workspace, Sentinel a Team Scaling







