01 - Módulos e Reutilização
video: https://youtu.be/... no frontmatter da nota.Módulos e Reutilização
Módulos são a unidade de reuso do IaC declarativo: pacotes de código que encapsulam um desenho de infraestrutura, por exemplo, uma VPC com sub-redes por zona, um cluster com auto scaling e observabilidade, um banco com backup e réplica, e o expõem como um contrato de entradas e saídas. A ideia central é transportar para infraestrutura a mecânica de bibliotecas de software: o desenho é escrito e testado uma vez, versionado, publicado e consumido por todos os ambientes e times, com divergência apenas pelos parâmetros declarados. Em produção, módulos são o que separa um repositório de IaC escalável de uma pasta de scripts crescentemente incontrolável, porque concentram decisão técnica em lugares revisáveis e distribuem execução em lugares parametrizáveis. O tema tem um lado de desenho que decide quase tudo: granularidade, tamanho do contrato e política de versionamento. Módulo bom é pequeno na interface e previsível no comportamento; módulo ruim é gigante, cheio de condicionais e impossível de evoluir sem quebrar consumidores. Este arquivo cobre o contrato, o versionamento, os anti-padrões e a composição de ambientes a partir de módulos.
Conceito: o módulo como API de infraestrutura
Um módulo é, antes de tudo, uma interface. A API pública é o conjunto de variáveis de entrada e de outputs; a implementação, os recursos declarados dentro, é detalhe encapsulado que pode evoluir livremente sem quebrar consumidores, desde que a interface não mude de forma incompatível. Pensar em módulo como API muda a conversa da equipe: o desenho de rede deixa de ser "aqueles 300 linhas na raiz" e vira um produto com assinatura, documentação e versionamento.
O consumidor de um módulo não deveria precisar ler a implementação para usá-lo corretamente. Nomes de variáveis com descrição, validações de entrada, valores padrão sensatos e exemplos de uso são parte do produto, não cortesia. Quando o consumo exige abrir o main.tf do módulo para descobrir o que cada variável faz, o contrato falhou, e o custo vai aparecer em cada consumidor novo.
A segunda propriedade essencial é a composição: módulos consomem módulos. Um módulo de plataforma compõe módulos de rede, cluster e observabilidade; uma raiz de ambiente compõe módulos de plataforma com parâmetros próprios. A hierarquia resultante é a estrutura viva do IaC, e cada nível de abstração deve expor menos detalhes que o nível de baixo, ou a composição não vale o custo.
No vocabulário de Terraform, a raiz de execução é o módulo raiz e todo o resto é módulo filho, mas a palavra-chave module é a menor parte da história: o que define módulo de qualidade é a interface, a política de versão e o dono. Essas três propriedades, e não a sintaxe, são o objeto de gestão de quem mantém um repositório de IaC em produção.
O problema que resolve: cópia e divergência
O cenário sem módulos é conhecido: um desenho funciona, alguém copia a pasta inteira para o segundo ambiente, o terceiro ambiente copia do segundo, e seis meses depois existem três variantes que divergiram em silêncio. A correção de uma vulnerabilidade precisa ser aplicada três vezes, manualmente, e ninguém tem certeza de que ficou igual; o ambiente esquecido acumula drift estrutural com origem em decisões nunca registradas.
O módulo ataca isso pela raiz: o desenho vive em um lugar, versionado, e os ambientes diferem apenas por parâmetros. A correção é um commit no módulo, um bump de versão nos consumidores e planos que provam a equivalência. A divergência passa a ser explícita e revisável, porque só existe nos argumentos declarados, e o custo marginal do ambiente novo cai para o custo de escrever parâmetros.
Há também o ganho de qualidade técnica concentrada. O desenho do módulo, digamos, de banco de dados, é revisado por quem entende de banco, testado em pipeline e usado em produção por vários ambientes antes de um consumidor novo adotá-lo. A cópia distribui risco; o módulo concentra e amadurece risco. Essa dinâmica é o argumento econômico central do reuso.
O contrato: input variables e outputs
Variáveis de entrada definem o que o consumidor precisa declarar: tipo, descrição, valor padrão e validação. A disciplina de contrato tem regras práticas: variáveis obrigatórias sem padrão para o que é identidade ou contexto, padrões sensatos para o que é política comum, validação para o que é barato de checar antes do apply, e descrição para tudo. Uma variável de tipo complexo com estrutura profunda costuma indicar que a abstração está errada.
Outputs definem o que o módulo expõe para fora: identificadores que outros módulos e raízes precisam consumir, arn, id, endpoint, e o mínimo necessário, não o inventário completo. Expor demais cria acoplamento por conveniência; o consumidor que lê um atributo interno deixa de ser isolado da implementação, e a próxima mudança interna quebra consumidores que "só olhavam". O output é promessa pública, e promessa pública tem custo de compatibilidade.
# modules/banco-gestito/variables.tf: o contrato do módulo
variable "nome" {
type = string
description = "Identificador do banco; inclui ambiente e domínio"
}
variable "tamanho" {
type = string
default = "small"
description = "Perfil de dimensão: small, medium ou large"
validation {
condition = contains(["small", "medium", "large"], var.tamanho)
error_message = "Perfil de dimensão inválido."
}
}
# modules/banco-gestito/main.tf: implementação encapsulada
resource "aws_db_instance" "este" {
identifier = var.nome
instance_class = local.classe_por_tamanho[var.tamanho] # política interna
engine = "postgres"
storage_encrypted = true
}
# modules/banco-gestito/outputs.tf: o que o módulo expõe
output "endpoint" {
value = aws_db_instance.este.endpoint
description = "Endpoint para a string de conexão da aplicação"
}
Versionamento e registry de módulos
Módulo sem versão é dependência volátil: o consumidor que aponta para a branch principal muda de infraestrutura sem commit próprio, e o plan de ontem não é o plan de hoje. A prática de produção é versionamento semântico: quebra de compatibilidade sobe major, adição de variável com padrão sobe minor, correção sobe patch, e o consumidor fixa a versão exata ou faixa estreita, com bump explícito no pull request.
O registry é o lugar de distribuição: público, como o registro de Terraform, ou privado, como registry corporativo, GitHub Packages, repositório Git com tags, ou ferramentas dedicadas. O mecanismo importa menos que a propriedade: versões imutáveis, changelog por release e documentação de interface gerada ou mantida junto. O registry privado é também o ponto de governança, com política de adoção e de aposentadoria de versões.
O teste de módulo é parte do release, não opcional: pipeline que executa plan em cenários representativos, de tamanho mínimo e de tamanho grande, compara resultados e publica a versão só com plano aprovado. Ferramentas do ecossistema automatizam esse ciclo com infraestrutura efêmera; o princípio, porém, é independente de ferramenta, pois módulo não testado é release sem evidência.
A aposentadoria de versões é o capítulo que times ignoram até sofrer: versões antigas com CVEs conhecidas continuam sendo consumidas, e o upgrade fica cada vez mais caro. A política saudável define janela de suporte, comunicação de deprecação nos próprios planos, alertas quando consumidor usa versão fora da janela, e prazo para remoção. Registry é contrato de longo prazo, não catálogo sem dono.
O anti-padrão do módulo gigante
O módulo gigante nasce de boa intenção: cobrir todos os casos. Ele acumula variáveis opcionais que ligam e desligam recursos, condicionais aninhadas, contagens dinâmicas e blocos que mudam de forma conforme flags. O resultado é o pior dos mundos: interface complexa como uma API genérica, implementação rígida como uma solução específica, e impossibilidade de raciocinar sobre o plano sem executar o módulo mentalmente em modo debugger.
O sinal clínico é o consumidor que passa trinta minutos escolhendo entre dezenas de variáveis, e o mantenedor que evita tocar no módulo porque qualquer mudança pode quebrar alguém em algum lugar. Os custos aparecem em todos os ciclos: revisão de plano lenta, testes combinatórios impossíveis, e evolução travada pela compatibilidade retroativa de uma interface que tentou ser tudo.
A alternativa é módulo focado: um desenho, poucas decisões, interface pequena. Variação legítima entre ambientes entra por parâmetro; variação de desenho não entra por flag, cria versão ou módulo novo. A regra prática é que o plano de um módulo bem desenhado deve ser explicável em uma frase por recurso, e a contagem de variáveis obrigatórias raramente passa de cinco.
Composição de ambientes a partir de módulos
A composição canônica tem três camadas: biblioteca de módulos de domínio, rede, banco, fila, cluster; módulos de plataforma que combinam domínios em unidades de produto; e raízes por ambiente que chamam plataforma com parâmetros. A raiz é fina por desenho: ela não desenha infraestrutura, ela instancia desenho com valores, e tudo que a raiz contém é parâmetro e conectividade entre módulos.
Essa hierarquia é o que torna a promoção de código entre ambientes honesta: o mesmo commit de módulo, com o mesmo desenho, é aplicado em staging e em produção, e o diff entre ambientes é exatamente o conjunto de parâmetros. Quando um ambiente precisa de desenho diferente, a diferença sobe para o módulo como decisão explícita, com revisão, e não se resolve editando a cópia local do ambiente.
# Raiz de produção: fina, compõe módulos com parâmetros
module "rede" {
source = "git::https://git.interno/infra/modulos.git//vpc?ref=v3.2.0"
nome = "producao"
cidr = "10.0.0.0/16"
azs = ["sa-east-1a", "sa-east-1b", "sa-east-1c"]
}
module "banco" {
source = "git::https://git.interno/infra/modulos.git//banco-gestito?ref=v2.3.0"
nome = "producao-catalogo"
tamanho = "large"
subnet_ids = module.rede.subnets_privadas
}
output "endpoint_banco" {
value = module.banco.endpoint # consumido pela pipeline da aplicação
}
Granularidade: comparando perfis de módulo
| Perfil | Escopo típico | Vantagem dominante | Risco dominante |
|---|---|---|---|
| Módulo atômico | Um recurso ou grupo mínimo | Simples, previsível, fácil de testar | Raízes verbosas, composição manual |
| Módulo de domínio | Um desenho completo, VPC, banco | Interface pequena, decisão concentrada | Escolha errada de fronteiras |
| Módulo de plataforma | Vários domínios combinados | Autonomia do consumidor, padrão único | Módulo gigante disfarçado |
| Copiar e colar | Sem módulo, raiz por pasta | Velocidade inicial | Divergência silenciosa, sem upgrade path |
A leitura de decisão: comece atômico ou de domínio, e deixe a plataforma emergir quando três raízes repetirem a mesma composição. Copiar e colar é aceitável no começo do projeto e é dívida clara depois; módulo de plataforma com vinte variáveis opcionais é dívida disfarçada de arquitetura.
Erros comuns e boas práticas
- Criar módulo por conveniência estética, com um recurso e uma variável que só repassa; abstração sem decisão encapsulada não paga o custo da indireção.
- Acumular variáveis de flag que ligam e desligam recursos; variação de desenho é versão nova ou módulo novo, não condicional no meio da implementação.
- Expor outputs internos por conveniência; cada output é promessa de compatibilidade, e a promessa deve ser mínima e intencional.
- Consumir módulo da branch principal; versão fixa com bump revisado é o único fluxo compatível com plan reproduzível.
- Publicar módulo sem changelog e sem documentação de interface; o custo reaparece em cada consumidor que abre a implementação para adivinhar.
- Testar módulo só na primeira vez que funcionou; módulo precisa de pipeline própria com plan em cenários representativos antes de cada release.
- Evoluir a interface sem política de deprecação; validação com mensagem clara e janela de transição evitam o upgrade emergencial da frota inteira.
- Deixar módulo sem dono; registry sem governança vira cemitério de versões, e a ausência de dono é o que permite CVE antiga viva no consumo.