Pular para o conteúdo

Terraform e OpenTofu

Intermediário24 min de leituraiac
Antes disto:

Terraform lê a sua descrição em HCL, consulta o estado atual através de um provider, calcula um plano de mudanças e aplica. Todo o resto — módulos, workspaces, funções — é detalhe em cima desse ciclo. Entenda o ciclo primeiro; ele explica quase todo comportamento estranho que você vai encontrar.

Em 2023 o Terraform mudou de licença open source para BUSL, e a comunidade criou o OpenTofu, um fork sob a Linux Foundation com licença MPL. Na prática de hoje: a linguagem é a mesma, os providers são os mesmos, e tofu funciona como substituto direto de terraform na maioria dos projetos.

Escolha OpenTofu se a licença importa para o seu jurídico ou se você quer recursos que ele adicionou (como criptografia de state nativa). Fique no Terraform se você depende do HCP Terraform ou de integração comercial. Não misture os dois no mesmo state.

O provider traduz HCL em chamadas de API. Fixe a versão dele — provider é código de terceiro com poder sobre a sua infraestrutura.

terraform {
required_version = "~> 1.9"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.60" }
}
}
provider "aws" {
region = var.regiao
default_tags { # tag em tudo, sem repetir em cada recurso
tags = { ambiente = var.ambiente, time = "loja", gerenciado_por = "terraform" }
}
}
resource "aws_s3_bucket" "relatorios" {
bucket = "empresa-relatorios-${var.ambiente}"
}
data "aws_vpc" "principal" { # data source: lê o que já existe, não cria
tags = { Name = "vpc-${var.ambiente}" }
}

Commite o .terraform.lock.hcl. Sem ele, dois apply da mesma revisão podem usar providers diferentes.

variable "ambiente" {
type = string
description = "Ambiente de destino."
validation { # falhe cedo, com mensagem útil
condition = contains(["dev", "hom", "prod"], var.ambiente)
error_message = "ambiente deve ser dev, hom ou prod."
}
}
locals {
nome_base = "loja-${var.ambiente}"
producao = var.ambiente == "prod"
}
output "bucket_arn" {
value = aws_s3_bucket.relatorios.arn
}
output "senha_inicial" {
value = random_password.banco.result
sensitive = true # esconde do log do CI — mas continua em claro no state
}

variable é entrada, local é expressão reaproveitada, output é interface para quem consome. Use validation sempre que houver valor inválido possível: erro de digitação barrado no plan custa segundos, aplicado custa um incidente.

Janela do terminal
terraform init # baixa providers e configura backend
terraform fmt -recursive && terraform validate # formato e sintaxe
terraform plan -out=plano.bin # calcula e SALVA o plano
terraform apply plano.bin # aplica exatamente aquele plano

Salvar o plano em arquivo e aplicar esse arquivo é a diferença entre revisar e torcer: sem -out, o apply recalcula e pode fazer algo diferente do que você leu. No pipeline, o plano vai como artefato para o pull request e o apply usa o mesmo arquivo aprovado.

Leia o resumo com atenção — os símbolos importam:

  • + cria, ~ altera no lugar, - destrói.
  • -/+ destrói e recria. É aqui que se perde banco de dados por causa de um campo que o provider não consegue atualizar. Se aparecer em recurso com estado, pare e investigue.

O Terraform monta um grafo a partir das referências. Sempre que possível, referencie em vez de repetir o valor:

resource "aws_instance" "app" {
subnet_id = aws_subnet.privada.id # dependência implícita: a ordem sai daqui
}
resource "aws_instance" "worker" {
depends_on = [aws_iam_role_policy.acesso_fila] # use só quando não há referência
}

depends_on em excesso serializa o apply e esconde erro de modelagem. Ele é para dependência que a API tem mas o código não expressa — permissão que precisa existir antes do recurso que a usa, tipicamente.

# for_each com mapa: a chave é o índice no state — renomear não recria os outros
resource "aws_s3_bucket" "por_time" {
for_each = toset(["loja", "pagamentos", "logistica"])
bucket = "empresa-${each.key}-${var.ambiente}"
}
# count: bom para "existe ou não existe"
resource "aws_cloudwatch_log_group" "auditoria" {
count = local.producao ? 1 : 0
name = "/auditoria/${local.nome_base}"
}

Prefira for_each a count para listas: com count, o recurso é endereçado por posição ([0], [1]), e remover o primeiro item recria todos os seguintes. Com for_each, o endereço é a chave.

Quase todo time começa com infraestrutura criada no console. Importe em vez de recriar:

# blocos de import são versionados e revisáveis — melhor que o comando avulso
import {
to = aws_s3_bucket.relatorios
id = "empresa-relatorios-prod"
}
Janela do terminal
terraform plan -generate-config-out=gerado.tf # esboça o HCL do recurso importado
terraform plan # o alvo é um plano SEM mudanças

Importe, ajuste o código até o plano ficar vazio, e só então siga. Plano com alteração logo após importar significa que o seu HCL não descreve o recurso real — aplicar ali é mudar produção sem querer.

Próximo passo: o arquivo que guarda tudo isso merece um capítulo próprio. Continue em State: onde tudo dá errado.