State do Terraform: Entendendo o Arquivo Mais Crítico do Projeto

[296] State do Terraform: Entendendo o Arquivo Mais Crítico do Projeto

O arquivo que liga o código à infraestrutura real: a anatomia do terraform.tfstate, os riscos do state local, o backend remoto em S3 com locking via DynamoDB, os comandos de inspeção e manipulação, workspaces, a estratégia de backup e o uso do state dentro de pipelines de CI/CD.
DevOps

19 min de leitura

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

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 apply simultâ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.

Comentários

Mais em DevOps

AIOps na Prática: O Que a IA Já Faz em Operações Hoje
AIOps na Prática: O Que a IA Já Faz em Operações Hoje

Avaliação honesta do AIOps em quatro faixas de maturidade: detecção de…

GitLab Self-Hosted: Soberania Total sobre Código e Pipelines
GitLab Self-Hosted: Soberania Total sobre Código e Pipelines

Quando o SaaS não é opção — setores regulados, residência de dados, custo por…

Capstone: Operações em Produção e Retrospectiva da Jornada
Capstone: Operações em Produção e Retrospectiva da Jornada

O sistema não termina no último deploy — começa: dashboards com as métricas…