Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

01 - Módulos e Reutilização

#composicao#modulos#outputs#registry#variables
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione 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.