Escrever um módulo que outros times usam
Extrair um módulo é fácil; fazer um módulo que outros times queiram usar é outra coisa. A diferença está na interface, no versionamento e na promessa de não quebrar quem depende dele.
Este guia extrai um módulo a partir de código que já existe — que é como isso acontece na prática.
1. Defina a interface antes do código
Seção intitulada “1. Defina a interface antes do código”Escreva primeiro a chamada que você gostaria de fazer. Se ela não couber em poucas linhas legíveis, a abstração está errada.
module "servico_web" { source = "git::https://github.com/empresa/tf-modulos.git//servico-web?ref=v1.0.0"
nome = "loja" ambiente = "prod" imagem = "registry.exemplo.com/loja@sha256:SUBSTITUA" dominio = "loja.exemplo.com.br" replicas = 4}Use nomes do domínio de quem consome (replicas, dominio), não do provider
(desired_count, alb_listener_rule_priority). Se o consumidor precisa entender o provider
para preencher, o módulo não abstraiu nada.
2. Estruture o repositório
Seção intitulada “2. Estruture o repositório”tf-modulos/ servico-web/ main.tf variables.tf outputs.tf versions.tf # required_version e required_providers README.md # gerado examples/ basico/main.tf # exemplo executável, usado nos testes tests/ padroes.tftest.hclO examples/basico não é enfeite: é o que o CI aplica para validar o módulo e o que a
pessoa copia para começar.
3. Variáveis com validação e padrão seguro
Seção intitulada “3. Variáveis com validação e padrão seguro”variable "nome" { type = string description = "Nome do serviço. Usado como prefixo dos recursos." validation { condition = can(regex("^[a-z][a-z0-9-]{2,29}$", var.nome)) error_message = "nome deve ter 3-30 caracteres minúsculos, começando por letra." }}
variable "ambiente" { type = string validation { condition = contains(["dev", "hom", "prod"], var.ambiente) error_message = "ambiente deve ser dev, hom ou prod." }}
variable "acesso_publico" { type = bool default = false # o padrão é o SEGURO; abrir é decisão explícita description = "Expõe o serviço à internet."}
variable "retencao_logs_dias" { type = number default = 14 validation { condition = var.retencao_logs_dias >= 7 error_message = "Retenção mínima de 7 dias por política interna." }}A regra que evita incidente: o caminho preguiçoso precisa ser o caminho seguro, porque é o que a maioria vai seguir. Bucket privado, criptografia ligada, proteção contra destruição — tudo isso por padrão.
4. Outputs úteis, e só
Seção intitulada “4. Outputs úteis, e só”output "url" { value = "https://${var.dominio}" }output "arn" { value = aws_ecs_service.este.id }output "secret_name" { value = aws_secretsmanager_secret.este.name }output "senha_inicial" { value = random_password.este.result sensitive = true # esconde do log; lembre que o state guarda em claro}Exponha o que o consumidor precisa para conectar outras coisas. Vinte outputs “por via das dúvidas” viram compromisso de compatibilidade que você não queria assumir.
5. Teste
Seção intitulada “5. Teste”# tests/padroes.tftest.hcl — roda com `terraform test`, sem provisionarrun "padroes_seguros" { command = plan variables { nome = "teste", ambiente = "dev", dominio = "teste.exemplo.com.br" }
assert { condition = aws_s3_bucket_public_access_block.este.block_public_acls == true error_message = "Bucket não pode permitir ACL pública por padrão." }}
run "nome_invalido_falha" { command = plan variables { nome = "TESTE_INVALIDO", ambiente = "dev", dominio = "x.exemplo.com.br" } expect_failures = [var.nome]}Teste também o caminho de erro: validação que ninguém exercita pode estar quebrada.
6. Documente a partir do código
Seção intitulada “6. Documente a partir do código”terraform-docs markdown table . > README.mdtflint --recursive && terraform fmt -check -recursive && terraform validateNo CI do repositório de módulos: fmt, validate, tflint, terraform test, e uma
verificação de que o README.md gerado está atualizado (se houver diferença, falhe o PR).
7. Versione e comunique mudanças
Seção intitulada “7. Versione e comunique mudanças”git tag v1.0.0 && git push --tagsmodule "servico_web" { source = "app.terraform.io/empresa/servico-web/aws" version = "~> 1.0" # aceita 1.x; nunca sobe para 2.0 sozinho}O contrato de compatibilidade: patch para correção sem mudança de interface, minor para variável nova com padrão, major para remoção, renomeação ou qualquer mudança que force recriação de recurso.
Nunca aponte para main: quem consome passaria a ter a infraestrutura alterada por um merge
que não viu. E publique um CHANGELOG.md com instrução de migração a cada major — quem vai
rodar plan amanhã precisa saber se aquele -/+ é esperado.
Se der errado
Seção intitulada “Se der errado”| Sintoma | Causa provável |
|---|---|
| Times copiam em vez de usar | A interface não cobre um caso comum; pergunte qual |
| Upgrade quebra o consumidor | Mudança que recria recurso lançada como minor |
| Módulo com 40 variáveis | Está espelhando o provider; abstraia ou divida |
plan diferente entre consumidores |
Versão de provider não fixada em versions.tf |
| Ninguém adota | Sem exemplo executável e sem README gerado |
Checklist de pronto
Seção intitulada “Checklist de pronto”- Chamada de exemplo escrita antes do código.
- Variáveis com descrição, validação e padrão seguro.
- Outputs mínimos e úteis, com
sensitiveonde couber. -
examples/basicoexecutável. -
terraform testcobrindo padrões e falhas esperadas. - README gerado e verificado no CI.
- Tag semântica publicada e consumo por
version, não por branch. - CHANGELOG com instrução de migração.