Guias / terraform
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.
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
| Comando | O que faz |
|---|---|
terraform init | Baixa providers e módulos, configura o backend do state. Roda uma vez por diretório (e de novo ao mudar providers) |
terraform fmt | Formata os arquivos no estilo padrão |
terraform validate | Verifica sintaxe e tipos, sem acessar a nuvem |
terraform plan | Lê o estado real dos recursos, compara com o código e mostra o que vai mudar. Não altera nada |
terraform apply | Executa o plano (pede confirmação) e atualiza o state |
terraform destroy | Destró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 }}terraform init # baixa o provider e cria o .terraform.lock.hclterraform plan # "Plan: 2 to add, 0 to change, 0 to destroy."terraform apply # confirme com "yes" e abra http://localhost:8080Agora veja o Terraform reagir a mudanças:
# 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 webterraform plan # o Terraform percebe que ele sumiu e planeja recriá-lo
terraform destroy # limpa tudoO 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ímbolo | Significa | Risco |
|---|---|---|
+ | Criar | Baixo |
~ | Alterar no lugar (update in-place) | Médio: confira o atributo |
-/+ | Substituir: destruir e criar de novo | Alto: 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 |
- | Destruir | Alto |
<= | Ler uma data source | Nenhum |
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:
terraform plan -out=tfplanterraform apply tfplan # aplica este plano, sem recalcularA linguagem: os blocos principais
| Bloco | Para quê |
|---|---|
terraform | Versão exigida do Terraform, providers e backend do state |
provider | Configuração de um provider (região, credenciais, alias) |
resource | Um objeto que o Terraform cria e gerencia |
data | Um objeto que já existe e só é lido (a AMI mais recente, uma zona DNS) |
variable | Entrada da configuração |
output | Valor exposto ao fim do apply ou para outra configuração |
locals | Valores intermediários, para não repetir expressões |
module | Uso 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).
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:
- Nunca no git. Ele contém atributos sensíveis em texto puro (senhas de banco, chaves geradas) e muda a cada
apply. - Nunca edite à mão. Use os comandos
terraform state(list,show,mv,rm) ou, melhor, os blocos de refatoração descritos adiante. - Em equipe, use um backend remoto com lock. Se duas pessoas rodam
applyao 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:
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
ephemeralexistem 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
planlento 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 Terraformimport { to = aws_s3_bucket.legacy id = "acme-legacy-bucket"}
# Parar de gerenciar sem destruir o recurso realremoved { from = aws_s3_bucket.old_logs lifecycle { destroy = false }}| Bloco | Desde | Para quê |
|---|---|---|
moved | 1.1 | Renomear ou mover recursos entre módulos |
import | 1.5 | Adotar 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 |
removed | 1.7 | Tirar 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-onlymostra o que mudou fora do Terraform, sem propor alterações nos recursos.- Rodar
planagendado 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:
| Camada | Ferramenta | O que pega |
|---|---|---|
| Formato e sintaxe | terraform fmt -check, terraform validate | Erros de digitação, tipos, referências inexistentes |
| Regras da entrada | Bloco validation em variáveis | Valores fora do permitido, antes de qualquer chamada |
| Garantias contínuas | Blocos check (1.5) | Asserções avaliadas a cada plan/apply, como “o site responde 200” |
| Testes | terraform test (1.6) | Comportamento do módulo, com command = plan (sem criar nada) ou apply (cria e destrói recursos reais) |
| Políticas | Linters 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:
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:
| Terraform | OpenTofu | |
|---|---|---|
| Licença | BSL 1.1 | MPL 2.0 |
| Mantenedor | HashiCorp (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 destaque | Integração com HCP Terraform (Stacks, políticas, drift), actions, terraform query | Criptografia 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ão | Data | Destaques |
|---|---|---|
| 1.10 | nov/2024 | Valores e recursos efêmeros; lock nativo no S3 (experimental) |
| 1.11 | fev/2025 | Atributos write-only; lock nativo no S3 estável e DynamoDB depreciado |
| 1.14 | nov/2025 | Bloco 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.16 | ago/2026 | import dentro de módulos; destroy = false no lifecycle |
| 1.17 | RC em out/2026 | Terraform 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
| Sintoma | O que geralmente é |
|---|---|
Error acquiring the state lock | Outra 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 mudou | Um 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 apply | A 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 result | Bug ou limitação do provider; atualize a versão dele |
Resource already exists no apply | O recurso foi criado à mão ou por outro state; adote com um bloco import |
Cycle no plano | Dois recursos se referenciam mutuamente; quebre o ciclo com um recurso separado (ex.: a regra de security group fora do grupo) |
| Plano lento demais | State 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.
applynã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 fluxo | Só AWS e o time quer a ferramenta nativa: CloudFormation ou CDK |
| Times que revisam infraestrutura em pull request | Um 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áveis | Quem quer infraestrutura em linguagem de programação: Pulumi ou CDK for Terraform |
| A base do cluster: rede, Kubernetes gerenciado, bancos | O 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
- Terraform in 100 Seconds (Fireship): a ideia geral em menos de dois minutos (é o vídeo embutido no topo).
Referências
- Documentação do Terraform: a referência oficial da linguagem, da CLI e dos backends.
- Tutorial de introdução com Docker: base do laboratório deste guia.
- Backend S3: configuração com
use_lockfilee a depreciação do DynamoDB. - Testes: a sintaxe de
.tftest.hcle mocks. - Importar recursos: o bloco
importe a geração de configuração. - Changelog do Terraform: o que mudou em cada versão.
- HashiCorp adota a BSL (em inglês): o anúncio da troca de licença, de agosto de 2023.
- OpenTofu: o fork open source, com a documentação e as diferenças em relação ao Terraform.
- Novidades da HashiConf 2025 (em inglês): Stacks estáveis, Terraform search e actions.
- IBM conclui a compra da HashiCorp (TechRepublic, em inglês).