The Code Codex
Tema
← Guias

Terraform

Infraestrutura como código com Terraform: o ciclo plan e apply, o state e o lock, o grafo de dependências, módulos, refatoração sem destruir recursos, testes, a licença BSL e o OpenTofu, com um laboratório local usando Docker.

21 out 2026· 18 min· 19 seções

Neste guia · 19 seções

O que é

O Terraform é uma ferramenta de infraestrutura como código criada pela HashiCorp em 2014. Você descreve em arquivos .tf a infraestrutura que quer (redes, máquinas, bancos, DNS, filas, usuários, até repositórios no GitHub) e o Terraform compara essa descrição com o que existe, mostra o que vai mudar e, com a sua confirmação, faz as chamadas de API necessárias.

Os arquivos são escritos em HCL (HashiCorp Configuration Language), uma linguagem declarativa feita para ser lida por pessoas. O nome vem de terraformar: transformar um planeta vazio num lugar habitável.

O problema

Sem uma ferramenta assim, a infraestrutura nasce de cliques no console da nuvem (o famoso ClickOps) ou de scripts imperativos. Os dois caminhos levam aos mesmos problemas:

  • Ninguém sabe exatamente o que existe. O conhecimento fica na cabeça de quem clicou.
  • Ambientes divergem. Homologação e produção deveriam ser iguais, mas cada um recebeu ajustes manuais diferentes.
  • Mudanças não são revisadas. Uma regra de firewall aberta demais vai para produção sem passar por ninguém.
  • Scripts não sabem o estado atual. Rodar duas vezes o script que cria uma VM cria duas VMs.
  • Recriar é arqueologia. Se uma conta é comprometida ou uma região cai, reconstruir o ambiente leva dias.

O Terraform resolve isso tratando a infraestrutura como código: versionada no git, revisada em pull request e aplicada de forma idempotente (aplicar duas vezes dá o mesmo resultado que aplicar uma).

Como funciona

O Terraform em si (o core) não sabe nada sobre AWS, Azure ou Docker. Ele lê a configuração, monta um grafo de dependências, compara com o state e delega cada operação a um provider: um plugin que traduz “crie este recurso” em chamadas à API de um serviço.

Arquivos .tf

estado desejado

Terraform core

grafo + plano

State

o que o Terraform

já criou

Provider aws

Provider github

Provider docker

API da AWS

API do GitHub

Docker local

Arquivos .tf

estado desejado

Terraform core

grafo + plano

State

o que o Terraform

já criou

Provider aws

Provider github

Provider docker

API da AWS

API do GitHub

Docker local

Os providers são baixados do Terraform Registry no terraform init. É por isso que o mesmo fluxo serve para qualquer coisa que tenha uma API: há providers oficiais e da comunidade para nuvens, bancos, SaaS, monitoramento e muito mais.

O ciclo de trabalho

ajustar

ok

Escreve

.tf

terraform init

baixa providers

terraform plan

calcula a diferença

Revisa

o plano

terraform apply

executa e grava o state

ajustar

ok

Escreve

.tf

terraform init

baixa providers

terraform plan

calcula a diferença

Revisa

o plano

terraform apply

executa e grava o state

ComandoO que faz
terraform initBaixa providers e módulos, configura o backend do state. Roda uma vez por diretório (e de novo ao mudar providers)
terraform fmtFormata os arquivos no estilo padrão
terraform validateVerifica sintaxe e tipos, sem acessar a nuvem
terraform planLê o estado real dos recursos, compara com o código e mostra o que vai mudar. Não altera nada
terraform applyExecuta o plano (pede confirmação) e atualiza o state
terraform destroyDestrói tudo o que esta configuração gerencia

A diferença entre plan e apply é o coração da segurança do Terraform: toda mudança é mostrada antes de acontecer.

Laboratório: um container com Terraform

Para experimentar sem conta na nuvem, o provider de Docker cria containers na sua máquina. Este exemplo é baseado no tutorial oficial de introdução da HashiCorp. Com Docker e Terraform instalados (brew install hashicorp/tap/terraform ou o binário em developer.hashicorp.com), crie uma pasta com um main.tf:

terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
provider "docker" {}
resource "docker_image" "nginx" {
name = "nginx:latest"
keep_locally = false
}
resource "docker_container" "web" {
name = "web"
image = docker_image.nginx.image_id # referência: cria a dependência
ports {
internal = 80
external = 8080
}
}
Terminal window
terraform init # baixa o provider e cria o .terraform.lock.hcl
terraform plan # "Plan: 2 to add, 0 to change, 0 to destroy."
terraform apply # confirme com "yes" e abra http://localhost:8080

Agora veja o Terraform reagir a mudanças:

Terminal window
# 1. Mude external = 8080 para 8081 no main.tf e rode:
terraform plan # o container será substituído (-/+): porta não muda "no lugar"
# 2. Apague o container por fora do Terraform e rode o plan de novo:
docker rm -f web
terraform plan # o Terraform percebe que ele sumiu e planeja recriá-lo
terraform destroy # limpa tudo

O segundo experimento mostra o drift: a realidade mudou sem passar pelo código, e o próximo plan revela a diferença.

Lendo um plano

O plano é o que você revisa antes de cada mudança, e cada símbolo tem um peso diferente:

SímboloSignificaRisco
+CriarBaixo
~Alterar no lugar (update in-place)Médio: confira o atributo
-/+Substituir: destruir e criar de novoAlto: o recurso some por um tempo e pode perder dados (um banco, um disco)
+/-Substituir criando o novo antes (com create_before_destroy)Médio
-DestruirAlto
<=Ler uma data sourceNenhum

A linha que mais merece atenção é # forces replacement: ela indica qual atributo obriga a substituição. Trocar o nome de um banco gerenciado, por exemplo, costuma significar apagar o banco.

Em times, o padrão é gravar o plano e aplicar exatamente o que foi revisado:

Terminal window
terraform plan -out=tfplan
terraform apply tfplan # aplica este plano, sem recalcular

A linguagem: os blocos principais

BlocoPara quê
terraformVersão exigida do Terraform, providers e backend do state
providerConfiguração de um provider (região, credenciais, alias)
resourceUm objeto que o Terraform cria e gerencia
dataUm objeto que já existe e só é lido (a AMI mais recente, uma zona DNS)
variableEntrada da configuração
outputValor exposto ao fim do apply ou para outra configuração
localsValores intermediários, para não repetir expressões
moduleUso de um conjunto reaproveitável de recursos

Um exemplo na AWS juntando os blocos:

variable "environment" {
type = string
default = "staging"
validation {
condition = contains(["staging", "production"], var.environment)
error_message = "Use staging ou production."
}
}
locals {
tags = { Environment = var.environment, ManagedBy = "terraform" }
}
data "aws_availability_zones" "available" {}
resource "aws_s3_bucket" "assets" {
bucket = "acme-assets-${var.environment}"
tags = local.tags
}
output "bucket_arn" {
value = aws_s3_bucket.assets.arn
}
Muitos recursos parecidos: count e for_each
resource "aws_s3_bucket" "logs" {
for_each = toset(["app", "audit", "billing"])
bucket = "acme-logs-${each.key}"
}

Prefira for_each a count para coleções. Com count, cada recurso é identificado pela posição (logs[0], logs[1]); remover o primeiro item da lista desloca todos os outros, e o Terraform planeja destruir e recriar recursos que não mudaram. Com for_each, a identidade é a chave (logs["audit"]) e não depende da ordem.

O grafo de dependências

Você não diz ao Terraform em que ordem criar as coisas. Cada referência de um recurso a outro (aws_subnet.private.id) vira uma aresta num grafo, e o Terraform cria primeiro o que não depende de nada, em paralelo sempre que possível (10 operações simultâneas por padrão).

depends_on

explícito

aws_vpc.main

aws_subnet.private

aws_security_group.app

aws_instance.app

aws_db_instance.main

aws_s3_bucket.assets

depends_on

explícito

aws_vpc.main

aws_subnet.private

aws_security_group.app

aws_instance.app

aws_db_instance.main

aws_s3_bucket.assets

Na destruição, o grafo é percorrido ao contrário: a instância some antes da subnet, que some antes da VPC. O depends_on existe para dependências que o Terraform não consegue enxergar pelas referências (uma permissão IAM que precisa existir antes de a aplicação subir, por exemplo). Use com moderação: ele torna o plano mais conservador.

Para ver o grafo da sua configuração: terraform graph | dot -Tsvg > graph.svg.

O state

O state (por padrão, o arquivo terraform.tfstate) é o mapa entre o seu código e o mundo real: ele registra que aws_instance.app no código corresponde à instância i-0abc123 na AWS, junto com todos os atributos conhecidos. Sem ele, o Terraform não saberia quais recursos são dele nem o que mudou.

Três regras sobre o state:

  1. Nunca no git. Ele contém atributos sensíveis em texto puro (senhas de banco, chaves geradas) e muda a cada apply.
  2. Nunca edite à mão. Use os comandos terraform state (list, show, mv, rm) ou, melhor, os blocos de refatoração descritos adiante.
  3. Em equipe, use um backend remoto com lock. Se duas pessoas rodam apply ao mesmo tempo com cópias locais do state, uma sobrescreve a outra.
Backend remoto e lock

Um backend remoto guarda o state num lugar compartilhado (S3, Azure Blob, GCS, HCP Terraform) e impede execuções simultâneas com um lock:

CIBackend S3AnaCIBackend S3Anaadquire lock (cria .tflock)okadquire lockerro: lock em uso por Analê state, aplica, grava state novolibera lockadquire lockok (lê o state já atualizado)
CIBackend S3AnaCIBackend S3Anaadquire lock (cria .tflock)okadquire lockerro: lock em uso por Analê state, aplica, grava state novolibera lockadquire lockok (lê o state já atualizado)

Na AWS, até pouco tempo atrás o lock exigia uma tabela no DynamoDB. Desde a versão 1.11 (fevereiro de 2025), o próprio S3 faz o lock com um arquivo .tflock, e o uso do DynamoDB para isso está depreciado:

terraform {
backend "s3" {
bucket = "acme-terraform-state"
key = "network/production.tfstate"
region = "us-east-1"
use_lockfile = true # lock nativo do S3 (1.11+)
}
}

Ative o versionamento e a criptografia no bucket: um state corrompido ou apagado se recupera da versão anterior.

Segredos fora do state

Historicamente, qualquer senha passada a um recurso acabava gravada no state. Duas adições recentes atacam o problema:

  • Valores e recursos efêmeros (1.10): variáveis, outputs e recursos marcados como ephemeral existem só durante a execução e não vão para o plano nem para o state (por exemplo, ler um segredo do cofre só para configurar um provider).
  • Atributos write-only (1.11): o provider pode declarar atributos que são enviados à API mas nunca gravados no state, como a senha inicial de um banco.

Ambos dependem de suporte do provider; atributos antigos continuam indo para o state.

Módulos

Um módulo é só uma pasta com arquivos .tf. Toda configuração já é um módulo (o root module); chamar outra pasta com o bloco module é como chamar uma função, com variáveis de entrada e outputs de saída:

module "network" {
source = "terraform-aws-modules/vpc/aws" # do Registry
version = "~> 6.0" # sempre fixe a versão
name = "acme-${var.environment}"
cidr = "10.0.0.0/16"
}
resource "aws_instance" "app" {
subnet_id = module.network.private_subnets[0]
# ...
}

O source pode ser uma pasta local (./modules/network), o Registry, um repositório git (git::https://...?ref=v1.2.0) ou um bucket.

Boas práticas que evitam dor:

  • Um módulo, uma responsabilidade (rede, banco, serviço). Módulos que fazem tudo viram parâmetros sem fim.
  • Fixe versões de módulos e providers, e faça commit do .terraform.lock.hcl, que trava a versão exata e os hashes dos providers.
  • States separados por ambiente e por camada. Um state único com toda a empresa deixa cada plan lento e cada erro perigoso. Dividir (rede, dados, aplicações; staging e produção) limita o raio de impacto.

Refatorando sem destruir

Como o state liga endereços do código a recursos reais, renomear um recurso no código parece, para o Terraform, “destruir o antigo e criar um novo”. Três blocos resolvem isso de forma declarativa, revisável no plano:

# Renomeei aws_instance.web para aws_instance.app (ou movi para um módulo)
moved {
from = aws_instance.web
to = aws_instance.app
}
# Um bucket criado à mão no console passa a ser gerenciado pelo Terraform
import {
to = aws_s3_bucket.legacy
id = "acme-legacy-bucket"
}
# Parar de gerenciar sem destruir o recurso real
removed {
from = aws_s3_bucket.old_logs
lifecycle {
destroy = false
}
}
BlocoDesdePara quê
moved1.1Renomear ou mover recursos entre módulos
import1.5Adotar recursos existentes; com terraform plan -generate-config-out=generated.tf, o Terraform escreve o resource para você. Desde a 1.16, funciona dentro de módulos
removed1.7Tirar do state sem destruir

Os comandos antigos (terraform import, terraform state mv) ainda existem, mas alteram o state na hora, sem plano e sem revisão. Os blocos entram no pull request como qualquer outra mudança.

Drift e o que o Terraform não faz

O Kubernetes também é declarativo, mas roda um loop de reconciliação contínuo: se alguém apaga um pod, outro nasce em segundos. O Terraform não: ele só compara e corrige quando alguém roda plan e apply. Entre uma execução e outra, mudanças manuais (drift) passam despercebidas.

Formas de lidar com isso:

  • terraform plan -refresh-only mostra o que mudou fora do Terraform, sem propor alterações nos recursos.
  • Rodar plan agendado no CI (diariamente, por exemplo) e alertar quando houver diferença. O HCP Terraform tem detecção de drift embutida.
  • Restringir permissões de escrita no console para as contas de produção: se só a pipeline pode alterar, o drift praticamente desaparece.

Testes e validação

Do mais barato ao mais caro:

CamadaFerramentaO que pega
Formato e sintaxeterraform fmt -check, terraform validateErros de digitação, tipos, referências inexistentes
Regras da entradaBloco validation em variáveisValores fora do permitido, antes de qualquer chamada
Garantias contínuasBlocos check (1.5)Asserções avaliadas a cada plan/apply, como “o site responde 200”
Testesterraform test (1.6)Comportamento do módulo, com command = plan (sem criar nada) ou apply (cria e destrói recursos reais)
PolíticasLinters e políticas como código (tflint, Checkov, OPA, Sentinel)Bucket público, recurso sem tag, instância grande demais

Um teste fica num arquivo .tftest.hcl:

tests/bucket.tftest.hcl
variables {
environment = "staging"
}
run "bucket_name_has_environment" {
command = plan
assert {
condition = aws_s3_bucket.assets.bucket == "acme-assets-staging"
error_message = "O nome do bucket deveria terminar com o ambiente."
}
}

Desde a 1.7, mock_provider permite testar sem credenciais, simulando as respostas do provider.

O fluxo em equipe

O padrão que funciona: ninguém roda apply em produção da própria máquina. Cada pull request executa fmt, validate, testes e plan, e o plano aparece como comentário no PR para revisão. O apply acontece só depois do merge, pela pipeline, com credenciais que só ela tem. Ferramentas como Atlantis, HCP Terraform, Spacelift e env0 automatizam esse fluxo.

Licença, IBM e OpenTofu

Até a versão 1.5, o Terraform era open source sob a licença MPL 2.0. Em agosto de 2023, a HashiCorp anunciou a troca para a Business Source License (BSL 1.1) a partir das versões seguintes. A BSL permite usar, modificar e redistribuir o Terraform, inclusive comercialmente, exceto para oferecer um produto que concorra com os da HashiCorp. Para quem só usa o Terraform para gerenciar a própria infraestrutura, nada muda na prática; para empresas que vendiam plataformas de automação de Terraform, mudou tudo.

A reação foi um fork: o OpenTofu, criado em agosto de 2023 (primeiro como OpenTF) e mantido sob a Linux Foundation, com licença MPL. Em fevereiro de 2025, a IBM concluiu a compra da HashiCorp por 6,4 bilhões de dólares.

Os dois projetos partiram do mesmo código e continuam muito parecidos (mesma linguagem, mesmos providers, comandos equivalentes com tofu no lugar de terraform), mas divergem a cada versão:

TerraformOpenTofu
LicençaBSL 1.1MPL 2.0
MantenedorHashiCorp (IBM)Comunidade, sob a Linux Foundation
Versão estável (out/2026)1.16 (1.17 em release candidate)1.13 (setembro de 2026)
Exclusivos de destaqueIntegração com HCP Terraform (Stacks, políticas, drift), actions, terraform queryCriptografia do state no próprio CLI (1.7), for_each em providers (1.9)

A migração de Terraform para OpenTofu (ou o contrário) é simples em configurações mais antigas e fica mais trabalhosa conforme você adota recursos exclusivos de um dos lados. Os números de versão não se correspondem: OpenTofu 1.13 não equivale a Terraform 1.13.

Novidades recentes

VersãoDataDestaques
1.10nov/2024Valores e recursos efêmeros; lock nativo no S3 (experimental)
1.11fev/2025Atributos write-only; lock nativo no S3 estável e DynamoDB depreciado
1.14nov/2025Bloco action para operações fora do ciclo de vida (ex.: invalidar um cache, disparar um playbook); list resources e terraform query para buscar e importar recursos existentes em massa
1.16ago/2026import dentro de módulos; destroy = false no lifecycle
1.17RC em out/2026Terraform Policy estável; -minimal-refresh, que atualiza só os recursos com mudanças propostas

Fora do CLI, o HCP Terraform (antigo Terraform Cloud) ganhou os Stacks, estáveis desde setembro de 2025: um jeito de implantar vários módulos e ambientes juntos, com as dependências entre eles resolvidas pela plataforma. É um recurso do serviço pago, não do binário.

Sintomas e causas

SintomaO que geralmente é
Error acquiring the state lockOutra execução está rodando, ou uma morreu sem liberar o lock. Confirme que não há ninguém aplicando antes de usar terraform force-unlock <id>
O plano quer substituir (-/+) algo que você não mudouUm atributo que força substituição mudou (às vezes por um default novo do provider), ou um count foi reordenado
O plano mostra mudanças toda vez, mesmo após o applyA API normaliza o valor (JSON reordenado, maiúsculas) e o provider não trata a diferença; use lifecycle { ignore_changes = [...] } com cuidado
Provider produced inconsistent resultBug ou limitação do provider; atualize a versão dele
Resource already exists no applyO recurso foi criado à mão ou por outro state; adote com um bloco import
Cycle no planoDois recursos se referenciam mutuamente; quebre o ciclo com um recurso separado (ex.: a regra de security group fora do grupo)
Plano lento demaisState grande demais; divida em configurações menores

Limites

  • Não é contínuo. Sem execuções periódicas, o drift fica invisível (veja acima).
  • O state é um ponto sensível. Ele concentra segredos e é a fonte da verdade; perder ou corromper o state é um incidente.
  • apply não é transacional. Se o apply falha no meio, o que já foi criado fica criado. O state registra o progresso, e o próximo apply continua de onde parou, mas não há rollback automático.
  • HCL não é uma linguagem de programação completa. Lógica complexa com for, condicionais e funções fica difícil de ler rápido. Quem prefere escrever infraestrutura em TypeScript, Python ou Go costuma escolher o Pulumi ou o CDK.
  • A qualidade depende do provider. Recursos novos de uma nuvem podem demorar a chegar ao provider, e cada provider tem suas peculiaridades.
  • Não configura o que roda dentro da máquina. Instalar pacotes e configurar o sistema operacional é trabalho para imagens prontas (Packer, containers) ou ferramentas de configuração (Ansible).

Quando usar (e quando não)

Use Terraform (ou OpenTofu)Prefira outra coisa
Infraestrutura em uma ou várias nuvens, mais SaaS (DNS, CDN, monitoramento, GitHub) no mesmo fluxoSó AWS e o time quer a ferramenta nativa: CloudFormation ou CDK
Times que revisam infraestrutura em pull requestUm projeto pequeno numa plataforma gerenciada (Vercel, Fly.io, Render), onde quase não há infraestrutura para descrever
Ambientes que precisam ser idênticos e recriáveisQuem quer infraestrutura em linguagem de programação: Pulumi ou CDK for Terraform
A base do cluster: rede, Kubernetes gerenciado, bancosO que roda dentro do cluster: manifestos, Helm e GitOps (Argo CD, Flux), ou Crossplane se a ideia é gerenciar a nuvem pelo próprio Kubernetes

Uma divisão comum: o Terraform cria a rede, o cluster Kubernetes e os bancos gerenciados; as aplicações dentro do cluster são implantadas por GitOps.

Veja também: Infraestrutura como código, Kubernetes, Entendendo containers e Os Doze Fatores.

Vídeos

Referências

1/19 · O que é