Pular para o conteúdo

Escrever um módulo que outros times usam

Avançado16 min de leituraiac

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.

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.

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.hcl

O examples/basico não é enfeite: é o que o CI aplica para validar o módulo e o que a pessoa copia para começar.

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.

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.

# tests/padroes.tftest.hcl — roda com `terraform test`, sem provisionar
run "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.

Janela do terminal
terraform-docs markdown table . > README.md
tflint --recursive && terraform fmt -check -recursive && terraform validate

No 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).

Janela do terminal
git tag v1.0.0 && git push --tags
module "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.

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
  • 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 sensitive onde couber.
  • examples/basico executável.
  • terraform test cobrindo 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.