Módulos de Terraform que escalam com a sua equipa, não contra ela
Um módulo de Terraform é a forma de uma equipa deixar de copiar e colar infra-estrutura. Mas um módulo que não é versionado, revisto e testado não escala a equipa — escala o raio do estrago. Uma alteração descuidada a um módulo 'vpc' partilhado e cada ambiente que aponta para ele herda o bug no próximo apply. A solução é deixar de tratar os módulos como pastas partilhadas e passar a tratá-los como produtos com versões.
Esta é a versão prática: o que torna um módulo fiável, como o publicar através de um pipeline a sério, e como as equipas o consomem — localmente e em CI — directamente do GitHub, fixado a uma tag ou, quando a segurança o exige, a um commit hash imutável. Sem precisar de um registry privado.
O que torna um módulo fiável
A fiabilidade começa antes de qualquer pipeline. Um módulo em que outras equipas confiam é pequeno e faz uma só coisa — uma rede, uma base de dados, um serviço — não uma 'plataforma' que faz tudo. Expõe um conjunto claro e documentado de inputs e outputs, traz defaults sensatos, e fixa as suas próprias versões de provider para se comportar igual hoje e daqui a seis meses.
- Responsabilidade única — um módulo, uma preocupação; compõem-se, em vez de aninhar um monólito.
- Interface explícita — variáveis tipadas com descrições e validação, e os outputs que outros módulos realmente precisam.
- Providers fixados — required_providers com restrições de versão, para que uma nova versão do provider não mude o comportamento sem avisar.
- Exemplos e documentação — uma pasta examples/ que é também aquilo contra o qual os testes correm.
- Sem surpresas — sem nomes, regiões ou IDs de conta fixos no código; nada que vá buscar coisas fora dos seus inputs.
Nada disto sobrevive a uma equipa em crescimento sem que duas coisas sejam inegociáveis: cada alteração é revista e cada alteração é testada contra infra-estrutura real e descartável. Um módulo é o único pedaço de código cujos bugs se multiplicam por cada consumidor — merece a fasquia mais alta que tiveres.
A versão é o contrato
À medida que a organização cresce e novos ambientes surgem — um sandbox aqui, uma segunda região ali — a versão deixa de ser um luxo e torna-se o contrato entre o módulo e toda a gente que o usa. Cada consumidor fixa uma versão explícita; ninguém segue a main. O versionamento semântico diz ao consumidor o que significa um salto: um patch é seguro, um minor acrescenta, um major obriga-o a mudar código.
Fixar é também como se avança e recua com confiança. Promover para produção é um salto de versão; uma má release é um salto de volta para a anterior. Sem versões, 'fazer rollback do módulo' quer mesmo dizer 'ir descobrir em que commit estava na terça-feira passada'.
Consome-o a partir do GitHub — sem registry
Não precisas de um registry privado nem de Artifactory para partilhar módulos como deve ser. Referencia o módulo directamente pela sua source de Git e por um ref. A mesma source funciona num portátil e em CI; a autenticação é só um token do GitHub — um PAT localmente, o token do runner em CI — ligado uma vez para que os clones por HTTPS levem a credencial.
# main.tf — fixar a uma tag publicada
module "vpc" {{
source = "git::https://github.com/acme/tf-modules.git//modules/vpc?ref=v1.4.0"
cidr_block = "10.20.0.0/16"
azs = ["eu-west-1a", "eu-west-1b"]
}}A credencial define-se uma vez com o insteadOf do git, por isso nem o teu código nem o teu state contêm um segredo — o Terraform limita-se a clonar por HTTPS em teu nome:
# local + CI: deixar o git usar um token para o github.com em HTTPS
git config --global url."https://oauth2:${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"Imutabilidade: fixa um commit hash quando tem de ficar trancado
Uma tag é cómoda, mas é móvel: qualquer pessoa com permissões de escrita pode re-apontar ou forçar a v1.4.0 para outro código, e o teu próximo apply puxa-o. Para a maioria dos módulos esse compromisso serve. Para os que não podem mudar debaixo dos teus pés — tudo o que é sensível em segurança ou conformidade — fixa antes o commit hash completo. Um commit não pode ser trocado: ref=<sha> resolve sempre exactamente para os bytes que revistes.
# trancado a um commit exacto e impossivel de trocar
module "kms" {
source = "git::https://github.com/acme/tf-modules.git//modules/kms?ref=3f9a1c4e8b7d6c5a4f3e2d1c0b9a8f7e6d5c4b3a"
}Publica o módulo como um produto
Se os consumidores fixam versões, algo tem de produzir essas versões de forma fiável — um pipeline no repositório do módulo. E vale a pena ser preciso: um módulo não 'se faz deploy'. É validado, testado e publicado. O deploy é trabalho do consumidor.
- Em cada pull request, corre as verificações estáticas: terraform fmt -check, terraform validate e tflint.
- Depois testa a sério — sobe o exemplo do módulo numa conta sandbox descartável, valida-o (terraform test, ou Terratest para verificações mais ricas) e destrói-o. Um módulo que nunca foi aplicado é um módulo por testar.
- No merge para a main, deixa o GitVersion calcular a próxima versão semântica a partir do histórico de commits, cria a tag de Git correspondente, e publica — anuncia a release no canal de Slack ou Teams da equipa (e espelha para um registry, se por acaso tiveres um).
# .github/workflows/release.yml (esboco)
on: { pull_request: {}, push: { branches: [main] } }
jobs:
check:
steps:
- run: terraform fmt -check -recursive
- run: terraform validate
- run: tflint
- run: terraform test # aplica os exemplos num sandbox e destroi
release:
if: github.ref == 'refs/heads/main'
needs: check
steps:
- uses: gittools/actions/gitversion/execute@v3 # -> proxima semver
- run: gh release create "v${VERSION}"
- run: ./notify.sh "tf-modules ${VERSION} publicado" # Slack / TeamsA ferramenta exacta não interessa — GitVersion, release-please e semantic-release servem todos. O que interessa é que uma versão só passa a existir depois de a alteração ter sido revista, validada e mesmo aplicada.
Agora usa-o: do Terraform ao Terragrunt
Com um módulo testado e com tag, um projecto de Terraform simples limita-se a fixá-lo — o bloco module acima é a história toda. Chega para um ou dois ambientes. Mas assim que corres a mesma stack em dev, staging e produção, numa ou mais regiões, copiar e colar essa ligação por ambiente é exactamente a duplicação que os módulos vieram matar.
O Terragrunt mantém os ambientes DRY
O Terragrunt envolve o Terraform para tirar essa repetição: gera a configuração de backend e de provider, mantém a versão do módulo num só sítio por stack, e deixa cada ambiente ser um ficheiro fino que diz 'este módulo, esta versão, estes inputs'. Promover na cadeia passa a ser um salto de versão de uma linha.
# live/prod/eu-west-1/vpc/terragrunt.hcl
terraform {
source = "git::https://github.com/acme/tf-modules.git//modules/vpc?ref=v1.4.0"
}
include "root" { path = find_in_parent_folders() }
inputs = {
cidr_block = "10.30.0.0/16"
azs = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
}Uma estrutura que escala: projecto / ambiente / região
Organiza o repositório live para que o caminho seja o endereço da infra-estrutura. Uma forma propositadamente aborrecida é ambiente → região → componente — adapta os níveis à forma como a tua organização realmente pensa (há quem separe primeiro por projecto, conta ou equipa):
live/
├── root.hcl # state remoto, provider, tags comuns
├── dev/
│ └── eu-west-1/
│ ├── vpc/terragrunt.hcl
│ └── eks/terragrunt.hcl
├── staging/
│ └── eu-west-1/ ...
└── prod/
├── eu-west-1/
│ ├── vpc/terragrunt.hcl
│ └── eks/terragrunt.hcl
└── eu-central-1/ ...Cada folha fixa uma versão do módulo, e a pasta diz-te exactamente o que corre onde. Actualizar a produção passa a ser um PR revisto que muda um ref= de v1.4.0 para v1.5.0 — pequeno, óbvio e trivial de reverter.
Módulos fiáveis e de propósito único; revistos e testados a cada alteração; versionados e publicados por um pipeline; consumidos do GitHub por tag — ou por commit hash imutável quando importa — e montados com uma estrutura de Terragrunt que reflecte o teu parque. É essa a diferença entre módulos que escalam com a tua equipa e módulos que, em silêncio, escalam contra ela.