Quando o Terraform aplica uma configuração e cria uma instância EC2, ele precisa de alguma forma lembrar que aquela instância existe — que ela corresponde ao recurso aws_instance.servidor_web declarado no código. Na próxima vez que terraform plan for executado, o Terraform precisa comparar o que está declarado no código com o que realmente existe na nuvem. Sem um registro persistente dessa correspondência, cada plan seria um processo cego, incapaz de determinar o que já foi criado e o que ainda precisa ser criado.
Esse registro é o state — um arquivo JSON chamado terraform.tfstate que mapeia cada recurso declarado no código a um recurso real na infraestrutura, armazenando os atributos desse recurso conforme foram lidos após a criação.
O state é o componente mais crítico de qualquer projeto Terraform. Perdê-lo significa perder a capacidade do Terraform de gerenciar a infraestrutura que ele mesmo criou — os recursos continuam existindo na nuvem, mas o Terraform não sabe disso. Corrompê-lo pode resultar em recursos sendo destruídos e recriados desnecessariamente, causando downtime.
Entender profundamente como o state funciona é o que separa alguém que usa Terraform de alguém que o domina.
Anatomia do Arquivo de State
O terraform.tfstate é um arquivo JSON com estrutura bem definida. Inspecionar o estado de um projeto simples revela sua estrutura:
{
"version": 4,
"terraform_version": "1.7.0",
"serial": 12,
"lineage": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"outputs": {
"ip_publico": {
"value": "54.123.45.67",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "servidor_web",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"ami": "ami-0c55b159cbfafe1f0",
"instance_type": "t3.micro",
"id": "i-0abc123def456789",
"public_ip": "54.123.45.67",
"private_ip": "10.0.1.45",
"tags": {
"Name": "servidor-web",
"Environment": "producao"
}
}
}
]
}
]
}
Alguns campos merecem atenção especial:
serial — um contador incrementado a cada modificação do state. O Terraform usa esse campo para detectar conflitos quando múltiplos usuários tentam modificar o state simultaneamente.
lineage — um UUID único gerado quando o state é criado pela primeira vez. Impede que estados de projetos diferentes sejam confundidos acidentalmente.
attributes — todos os atributos do recurso conforme lidos da API da AWS após a criação. É com esses valores que o Terraform compara o estado desejado durante o plan.
O arquivo de state nunca deve ser editado manualmente exceto em situações de emergência muito específicas, usando sempre os comandos apropriados do Terraform.
O Problema do State Local
Por padrão, o Terraform armazena o state em um arquivo local chamado terraform.tfstate no diretório de trabalho. Esse comportamento é adequado para aprendizado e experimentos individuais, mas é completamente inadequado para uso em equipe por três razões fundamentais.
Ausência de locking — se dois engenheiros executam terraform apply simultaneamente, ambos leem o mesmo state, fazem mudanças e tentam escrever de volta. O resultado é corrupção do state ou sobrescrita de mudanças. Em uma infraestrutura de produção, esse cenário pode causar recursos duplicados, recursos destruídos acidentalmente ou inconsistências que levam horas para diagnosticar.
Sem compartilhamento — o state local existe apenas na máquina de quem executou o apply. Outros membros da equipe não têm acesso a ele. Se a máquina falha ou o arquivo é deletado, a capacidade de gerenciar a infraestrutura é perdida.
Risco de versionamento acidental — o terraform.tfstate frequentemente contém senhas, tokens e outros valores sensíveis extraídos dos recursos durante o apply. Se esse arquivo for commitado acidentalmente no Git — e isso acontece com mais frequência do que se imagina — esses segredos ficam expostos no histórico do repositório para sempre.
A solução para todos esses problemas é o backend remoto.
Configurando um Backend Remoto com S3 e DynamoDB
O backend mais comum para equipes que usam AWS combina um bucket S3 para armazenamento do state com uma tabela DynamoDB para locking distribuído. O S3 resolve o problema de compartilhamento e o DynamoDB resolve o problema de concorrência.
Primeiro, cria-se a infraestrutura necessária para o backend — ironicamente, isso é feito uma única vez de forma manual ou com um projeto Terraform bootstrapper separado:
# bootstrap/main.tf
# Este projeto é executado apenas uma vez para criar a infraestrutura
# necessária para armazenar o state dos demais projetos
provider "aws" {
region = "us-east-1"
}
# Bucket S3 para armazenamento do state
resource "aws_s3_bucket" "terraform_state" {
bucket = "minha-empresa-terraform-state"
# Impede destruição acidental
lifecycle {
prevent_destroy = true
}
tags = {
Name = "Terraform State"
ManagedBy = "terraform"
}
}
# Habilita versionamento — permite recuperar versões anteriores do state
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
# Criptografia server-side — o state pode conter senhas e tokens
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 = "AES256"
}
}
}
# Bloqueia todo acesso público ao bucket
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
}
# Tabela DynamoDB para locking de state
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-state-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
lifecycle {
prevent_destroy = true
}
tags = {
Name = "Terraform State Locks"
ManagedBy = "terraform"
}
}
Com a infraestrutura de backend criada, configura-se o backend nos projetos:
# versions.tf de qualquer projeto que use este backend
terraform {
required_version = ">= 1.7.0"
backend "s3" {
bucket = "minha-empresa-terraform-state"
key = "projetos/minha-api/staging/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-locks"
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.31"
}
}
}
A estrutura de chaves no S3 deve refletir a hierarquia da organização:
terraform-state/
├── projetos/
│ ├── minha-api/
│ │ ├── staging/terraform.tfstate
│ │ └── production/terraform.tfstate
│ └── sistema-pagamentos/
│ ├── staging/terraform.tfstate
│ └── production/terraform.tfstate
└── shared/
├── ecr/terraform.tfstate
└── route53/terraform.tfstate
Como Funciona o State Locking
Quando terraform apply é executado com o backend S3 + DynamoDB configurado, o processo é:
1. Terraform tenta adquirir o lock
→ Cria um item na tabela DynamoDB com o LockID do state
2. Se o lock está disponível:
→ Lê o state do S3
→ Executa o plan
→ Aplica as mudanças
→ Escreve o novo state no S3
→ Libera o lock (remove o item do DynamoDB)
3. Se o lock está ocupado:
→ Exibe mensagem com informações de quem detém o lock
→ Aguarda ou aborta (dependendo da configuração)
Quando um apply é interrompido abruptamente — queda de energia, Ctrl+C, falha de rede — o lock pode ficar preso. Para forçar a liberação após confirmar que nenhum outro processo está rodando:
# Exibe informações sobre o lock atual
terraform force-unlock LOCK_ID
# O LOCK_ID está disponível na mensagem de erro quando o lock está ocupado
# Exemplo de mensagem:
# Error: Error acquiring the state lock
# Lock Info:
# ID: 8a6e0731-6b9a-4c3a-8d9c-1f2e3a4b5c6d
# Path: projetos/minha-api/staging/terraform.tfstate
# Operation: OperationTypeApply
# Who: joao@notebook-joao
# Created: 2025-03-10 14:30:00
Comandos de Inspeção e Manipulação do State
O Terraform fornece um conjunto de comandos para inspecionar e manipular o state sem editá-lo diretamente.
Listando recursos no state:
# Lista todos os recursos gerenciados pelo state atual
terraform state list
# Saída típica:
# aws_instance.servidor_web
# aws_security_group.aplicacao
# aws_db_instance.principal
# module.vpc.aws_vpc.this
# module.vpc.aws_subnet.publica[0]
# module.vpc.aws_subnet.publica[1]
# module.vpc.aws_nat_gateway.this[0]
Inspecionando um recurso específico:
# Exibe todos os atributos de um recurso no state
terraform state show aws_instance.servidor_web
# Exibe recurso dentro de um módulo
terraform state show 'module.vpc.aws_vpc.this'
Movendo recursos no state:
O comando terraform state mv é usado quando o código é refatorado — renomear um recurso ou movê-lo para dentro de um módulo — sem querer destruir e recriar o recurso real:
# Renomeia um recurso no state
terraform state mv \
aws_instance.servidor_web \
aws_instance.servidor_web_principal
# Move um recurso para dentro de um módulo
terraform state mv \
aws_security_group.aplicacao \
module.aplicacao.aws_security_group.this
# Move recurso entre states (de um projeto para outro)
terraform state mv \
-state-out=../outro-projeto/terraform.tfstate \
aws_s3_bucket.assets \
aws_s3_bucket.assets
Removendo recursos do state sem destruí-los:
Quando um recurso precisa deixar de ser gerenciado pelo Terraform — por exemplo, para ser importado em outro projeto — usa-se terraform state rm:
# Remove o recurso do state mas não o destrói na infraestrutura real
terraform state rm aws_instance.servidor_legado
# Remove todos os recursos de um módulo
terraform state rm 'module.vpc'
Importando recursos existentes:
Quando existe infraestrutura criada fora do Terraform que precisa passar a ser gerenciada por ele, usa-se terraform import:
# Importa uma instância EC2 existente
terraform import aws_instance.servidor_legado i-0abc123def456789
# Importa um bucket S3
terraform import aws_s3_bucket.legado nome-do-bucket-existente
# Importa um registro de DNS do Route53
terraform import aws_route53_record.api ZONE_ID_RECORD_ID_TYPE
A partir da versão 1.5, o Terraform suporta um bloco import declarativo nos arquivos .tf, que é preferível ao comando imperativo em projetos novos:
# Bloco de import declarativo — Terraform 1.5+
import {
to = aws_instance.servidor_legado
id = "i-0abc123def456789"
}
import {
to = aws_s3_bucket.legado
id = "nome-do-bucket-existente"
}
Workspaces: Múltiplos States em Uma Configuração
O Terraform suporta workspaces — múltiplos states associados à mesma configuração. Cada workspace tem seu próprio arquivo de state, permitindo usar a mesma configuração para diferentes ambientes:
# Lista os workspaces existentes
terraform workspace list
# Cria e muda para um novo workspace
terraform workspace new staging
terraform workspace new production
# Muda entre workspaces
terraform workspace select staging
# Exibe o workspace atual
terraform workspace show
Dentro da configuração, o workspace atual pode ser referenciado via terraform.workspace:
locals {
ambiente = terraform.workspace
config = {
development = {
instance_type = "t3.micro"
min_size = 1
max_size = 2
}
staging = {
instance_type = "t3.small"
min_size = 1
max_size = 3
}
production = {
instance_type = "t3.large"
min_size = 2
max_size = 10
}
}
}
resource "aws_instance" "app" {
instance_type = local.config[local.ambiente].instance_type
# ...
}
Embora workspaces sejam úteis para casos simples, a maioria das organizações prefere diretórios separados por ambiente — como mostrado na estrutura de módulos do artigo Módulos no Terraform: Reusabilidade e Organização. Diretórios separados têm configurações explícitas por ambiente, facilitam diferenças maiores entre ambientes e tornam o pipeline de CI/CD mais previsível.
Estratégia de Backup e Recuperação de State
Mesmo com o versionamento habilitado no S3, uma estratégia explícita de backup protege contra cenários de recuperação de desastre:
# Faz backup manual do state atual
terraform state pull > backup-$(date +%Y%m%d-%H%M%S).tfstate
# Restaura um state a partir de um backup
terraform state push backup-20250310-143000.tfstate
# Lista versões anteriores do state no S3
aws s3api list-object-versions \
--bucket minha-empresa-terraform-state \
--prefix projetos/minha-api/staging/terraform.tfstate \
--query 'Versions[*].{VersionId:VersionId,LastModified:LastModified}' \
--output table
# Restaura uma versão específica do S3
aws s3api get-object \
--bucket minha-empresa-terraform-state \
--key projetos/minha-api/staging/terraform.tfstate \
--version-id "abc123def456" \
state-restaurado.tfstate
# Verifica a integridade antes de restaurar
terraform state pull > state-atual.tfstate
diff state-atual.tfstate state-restaurado.tfstate
# Aplica o state restaurado
terraform state push state-restaurado.tfstate
State em Pipelines de CI/CD
Em pipelines de CI/CD, o Terraform precisa de acesso ao backend remoto sem interação humana. A configuração do backend pode receber variáveis via flags na linha de comando — útil quando partes da configuração variam entre pipelines:
# .github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [main]
paths: ['infrastructure/**']
jobs:
terraform:
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: infrastructure/environments/production
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.7.0"
- name: Configura credenciais AWS via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_TERRAFORM_ROLE_ARN }}
aws-region: us-east-1
- name: Terraform Init
run: |
terraform init \
-backend-config="bucket=minha-empresa-terraform-state" \
-backend-config="key=projetos/minha-api/production/terraform.tfstate" \
-backend-config="region=us-east-1" \
-backend-config="dynamodb_table=terraform-state-locks"
- name: Terraform Validate
run: terraform validate
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Plan
id: plan
run: |
terraform plan \
-var="db_password=${{ secrets.DB_PASSWORD }}" \
-out=tfplan \
-no-color 2>&1 | tee plan-output.txt
- name: Publica plano como comentário no PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const planOutput = fs.readFileSync('infrastructure/environments/production/plan-output.txt', 'utf8');
const truncated = planOutput.length > 60000
? planOutput.substring(0, 60000) + '\n... (truncado)'
: planOutput;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Terraform Plan — Produção\n\`\`\`\n${truncated}\n\`\`\``
});
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve tfplan
O Que Vem a Seguir
O próximo artigo introduz o Ansible — a ferramenta que complementa o Terraform gerenciando o que acontece dentro dos servidores após eles serem provisionados. Enquanto o Terraform cria e gerencia a infraestrutura, o Ansible configura os servidores: instala pacotes, copia arquivos, inicia serviços e garante que o estado interno de cada máquina corresponde ao declarado.
Referências para Aprofundamento
Documentação oficial
- Terraform State — developer.hashicorp.com — Documentação completa sobre o state do Terraform, incluindo backends disponíveis, locking e manipulação de state.
- Terraform Backend S3 — developer.hashicorp.com — Referência completa do backend S3, cobrindo todas as opções de configuração incluindo assumeRole e workspaces.
- Terraform Import — developer.hashicorp.com — Documentação do bloco de import declarativo introduzido no Terraform 1.5, com exemplos práticos.
- HashiCorp — Remote State: https://developer.hashicorp.com/terraform/language/state/remote
Exercícios
Exercício 1
O que exatamente o Terraform perde se o arquivo de state for apagado — e o que acontece com os recursos na nuvem? Liste as três razões pelas quais o state local é inadequado para uso em equipe.
Ver resposta
✓ Resposta: Perde-se o mapeamento entre o que está declarado no código e o que existe de verdade na nuvem. Os recursos continuam existindo e funcionando — instâncias rodando, bancos ativos, faturas chegando —, mas o Terraform passa a ignorá-los: no próximo plan ele conclui que nada foi criado ainda e propõe criar tudo de novo, gerando recursos duplicados ao lado dos órfãos. Recuperar significa reimportar cada recurso, um a um.
As três razões contra o state local:
- Ausência de locking — dois
applysimultâneos leem o mesmo state e escrevem por cima um do outro, corrompendo-o ou perdendo mudanças. - Sem compartilhamento — o state existe só na máquina de quem aplicou; se ela falhar ou o arquivo sumir, a equipe perde a capacidade de gerenciar a infraestrutura.
- Risco de versionamento acidental — o state guarda senhas e tokens lidos dos recursos; commitado por engano, o segredo fica no histórico do Git para sempre.
Exercício 2
Para que servem os campos serial e lineage do arquivo de state? Que problema cada um previne?
Ver resposta
✓ Resposta: O serial é um contador incrementado a cada modificação do state. Ele permite detectar conflito de concorrência: se o Terraform leu o state com serial: 12 e, na hora de gravar, encontra um número maior, significa que outra pessoa escreveu nesse intervalo — e a escrita é recusada em vez de sobrescrever silenciosamente o trabalho alheio.
O lineage é um UUID gerado uma única vez, quando o state nasce, e preservado em todas as gravações seguintes. Ele previne um erro de natureza diferente: usar o state errado. Se alguém apontar a configuração para o state de outro projeto — um key copiado e não ajustado no backend, um state push com o arquivo trocado —, as linhagens não batem e o Terraform recusa a operação. Sem isso, ele compararia recursos de um projeto com o state de outro e proporia destruir tudo que "sobra".
São proteções complementares: serial cuida do quando (versão), lineage cuida do qual (identidade).
Exercício 3
Você renomeou aws_instance.servidor_web para aws_instance.servidor_web_principal no código, sem mudar nenhum atributo. O que o terraform plan propõe se você rodar direto, e por quê? Qual comando evita isso, e em que ele difere de terraform state rm?
Ver resposta
✓ Resposta: O plan propõe destruir a instância e criar outra idêntica. O Terraform identifica recursos pelo endereço no state, não pelo conteúdo: ele vê que aws_instance.servidor_web existe no state mas sumiu do código (logo, deve ser destruído) e que aws_instance.servidor_web_principal está no código mas não no state (logo, deve ser criado). Ele não tem como saber que é a mesma máquina com outro nome — e o resultado é downtime, IP novo e perda do que estivesse em disco, tudo por uma renomeação puramente cosmética.
A solução é terraform state mv aws_instance.servidor_web aws_instance.servidor_web_principal, que reescreve o endereço dentro do state. Depois disso o plan reporta No changes, porque código e state voltam a se corresponder. O mesmo vale ao mover um recurso para dentro de um módulo, quando o endereço ganha o prefixo module..
A diferença para o state rm: o mv preserva o vínculo, apenas mudando o endereço; o rm desfaz o vínculo, tirando o recurso do state sem tocá-lo na nuvem. Depois de um rm, o recurso vira órfão — continua existindo e cobrando, mas o Terraform não o gerencia mais e proporia criar outro no lugar. O rm serve quando se quer deliberadamente entregar o recurso a outro projeto, geralmente seguido de um import do outro lado.
Exercício 4
Descreva o que acontece na tabela DynamoDB durante um terraform apply e por que o S3 sozinho não resolveria o problema de concorrência. Por que tanto o bucket quanto a tabela declaram lifecycle { prevent_destroy = true }?
Ver resposta
✓ Resposta: Antes de qualquer coisa, o Terraform tenta adquirir o lock, criando um item na tabela cuja chave (LockID) identifica aquele state. Se conseguir, lê o state do S3, executa o plano, aplica as mudanças, grava o state de volta e por fim remove o item, liberando o lock. Se o item já existir, outra execução está em andamento e o Terraform aborta informando quem detém o lock e desde quando.
O S3 sozinho não resolve porque ele guarda o arquivo, mas não oferece a operação atômica de "criar apenas se ainda não existir" com a semântica de exclusão mútua que o fluxo exige. Dois apply simultâneos leriam a mesma versão do state e ambos escreveriam — o último sobrescrevendo o primeiro. O DynamoDB fornece essa escrita condicional atômica, que é o que transforma duas execuções paralelas em uma fila.
O prevent_destroy está nos dois porque eles são a fundação de todos os outros projetos. Um terraform destroy distraído no projeto de bootstrap apagaria o bucket com o state de toda a organização — e, sem state, o Terraform perde o controle de toda a infraestrutura de uma vez. Com a diretiva, qualquer plano que tente destruí-los falha antes de executar. É a proteção da proteção: o versionamento do bucket permite recuperar uma versão ruim do state, mas não recupera o bucket inteiro se ele deixar de existir.
Exercício 5
Workspaces e diretórios separados por ambiente resolvem o mesmo problema de formas diferentes. Explique como cada abordagem funciona e por que o artigo diz que a maioria das organizações prefere a segunda.
Ver resposta
✓ Resposta: Com workspaces, uma única configuração mantém vários states — um por workspace. As diferenças entre ambientes ficam dentro do código, tipicamente num mapa indexado por terraform.workspace, como o local.config[local.ambiente] que escolhe t3.micro, t3.small ou t3.large. Troca-se de ambiente com terraform workspace select.
Com diretórios separados, cada ambiente é um projeto raiz próprio (environments/staging, environments/production), com seu backend, seu key no S3 e sua chamada explícita aos módulos compartilhados.
A preferência pelos diretórios tem três motivos. A configuração fica explícita: abrir environments/production/main.tf mostra o que existe em produção, sem precisar simular mentalmente um mapa. Comporta diferenças estruturais, não só de valores — produção pode ter recursos que staging não tem, algo que o mapa por workspace não expressa bem. E torna o pipeline previsível, porque o diretório determina o ambiente; com workspaces, o alvo depende de um estado de sessão (workspace select), e esquecer de trocar significa aplicar em produção achando que está em staging — um erro fácil de cometer e caro de descobrir.