Terraform e OpenTofu
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.
Terraform ou OpenTofu
Seção intitulada “Terraform ou OpenTofu”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.
Providers e resources
Seção intitulada “Providers e resources”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.
Variáveis, locals e outputs
Seção intitulada “Variáveis, locals e outputs”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.
O ciclo plan e apply
Seção intitulada “O ciclo plan e apply”terraform init # baixa providers e configura backendterraform fmt -recursive && terraform validate # formato e sintaxeterraform plan -out=plano.bin # calcula e SALVA o planoterraform apply plano.bin # aplica exatamente aquele planoSalvar 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.
Dependências implícitas e explícitas
Seção intitulada “Dependências implícitas e explícitas”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.
count, for_each e blocos dinâmicos
Seção intitulada “count, for_each e blocos dinâmicos”# for_each com mapa: a chave é o índice no state — renomear não recria os outrosresource "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.
Importando recurso existente
Seção intitulada “Importando recurso existente”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 avulsoimport { to = aws_s3_bucket.relatorios id = "empresa-relatorios-prod"}terraform plan -generate-config-out=gerado.tf # esboça o HCL do recurso importadoterraform plan # o alvo é um plano SEM mudançasImporte, 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.