DevOps

Artigo 27 — Módulos no Terraform: Reusabilidade e Organização Já leu

16 min de leitura

Artigo 27 — Módulos no Terraform: Reusabilidade e Organização
Artigo 27 — Módulos no Terraform: Reusabilidade e Organização O Problema da Repetição em Infraestrutura Nos artigos anteriores, toda a infraestrutura foi

Artigo 27 — Módulos no Terraform: Reusabilidade e Organização

Módulo 5 · Infraestrutura como Código Prof. Ricardo Matos — Dominando DevOps & Cloud em 1 Ano


O Problema da Repetição em Infraestrutura

Nos artigos anteriores, toda a infraestrutura foi escrita em um único conjunto de arquivos. Essa abordagem funciona bem para um projeto isolado, mas rapidamente se torna insustentável quando a organização cresce.

Imagine uma empresa com cinco sistemas diferentes, cada um com três ambientes — desenvolvimento, staging e produção. São quinze configurações de infraestrutura. Se cada uma delas precisar de uma VPC com subnets públicas e privadas, grupos de segurança, NAT Gateways e tabelas de rotas, o código se repete quinze vezes. Quando o padrão de segurança muda — e ele sempre muda — a atualização precisa ser replicada manualmente em todas as quinze configurações. O risco de inconsistências é alto e o custo de manutenção é proibitivo.

Módulos são a solução do Terraform para esse problema. Um módulo é um conjunto de arquivos de configuração Terraform agrupados em um diretório, que encapsula um conjunto de recursos relacionados e os expõe como uma unidade reutilizável com uma interface bem definida — entradas, saídas e comportamento.

A analogia com programação é direta: módulos são funções ou classes da linguagem de infraestrutura. Assim como uma função encapsula lógica reutilizável, um módulo encapsula infraestrutura reutilizável.


Anatomia de um Módulo

Todo diretório contendo arquivos .tf é um módulo. O que diferencia um módulo bem construído de um mal construído é a clareza da sua interface pública — as variáveis que aceita como entrada e os outputs que expõe como saída.

A estrutura recomendada para um módulo:

modules/
└── vpc/
    ├── main.tf        # Recursos do módulo
    ├── variables.tf   # Interface de entrada — o que o módulo aceita
    ├── outputs.tf     # Interface de saída — o que o módulo expõe
    ├── versions.tf    # Requisitos de versão
    └── README.md      # Documentação do módulo

O README.md não é opcional em módulos que serão compartilhados. Ele deve documentar o propósito do módulo, os inputs obrigatórios e opcionais, os outputs e um exemplo de uso.


Criando um Módulo de VPC

Transformando a configuração de VPC do artigo anterior em um módulo reutilizável:

# modules/vpc/variables.tf

variable "project_name" {
  description = "Nome do projeto — usado como prefixo nos nomes dos recursos"
  type        = string
}

variable "environment" {
  description = "Ambiente de implantação"
  type        = string

  validation {
    condition     = contains(["development", "staging", "production"], var.environment)
    error_message = "Ambiente deve ser development, staging ou production."
  }
}

variable "vpc_cidr" {
  description = "Bloco CIDR da VPC"
  type        = string
  default     = "10.0.0.0/16"

  validation {
    condition     = can(cidrnetmask(var.vpc_cidr))
    error_message = "O vpc_cidr deve ser um bloco CIDR válido."
  }
}

variable "availability_zones" {
  description = "Lista de availability zones para distribuição das subnets"
  type        = list(string)

  validation {
    condition     = length(var.availability_zones) >= 2
    error_message = "Pelo menos duas availability zones são necessárias para alta disponibilidade."
  }
}

variable "habilitar_nat_gateway" {
  description = "Se verdadeiro, cria NAT Gateways para as subnets privadas"
  type        = bool
  default     = true
}

variable "nat_gateway_por_az" {
  description = "Se verdadeiro, cria um NAT Gateway por AZ. Se falso, usa apenas um (menor custo, menor resiliência)"
  type        = bool
  default     = true
}

variable "tags_adicionais" {
  description = "Tags adicionais a serem aplicadas em todos os recursos"
  type        = map(string)
  default     = {}
}
# modules/vpc/main.tf

locals {
  # Determina quantos NAT Gateways criar
  qtd_nat_gateways = var.habilitar_nat_gateway ? (
    var.nat_gateway_por_az ? length(var.availability_zones) : 1
  ) : 0

  # Merge das tags padrão com as adicionais
  tags_comuns = merge(
    {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "terraform"
      Module      = "vpc"
    },
    var.tags_adicionais
  )
}

resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(local.tags_comuns, {
    Name = "${var.project_name}-${var.environment}-vpc"
  })
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id

  tags = merge(local.tags_comuns, {
    Name = "${var.project_name}-${var.environment}-igw"
  })
}

resource "aws_subnet" "publica" {
  count = length(var.availability_zones)

  vpc_id                  = aws_vpc.this.id
  cidr_block              = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = merge(local.tags_comuns, {
    Name = "${var.project_name}-${var.environment}-subnet-pub-${count.index + 1}"
    Tier = "public"
  })
}

resource "aws_subnet" "privada" {
  count = length(var.availability_zones)

  vpc_id            = aws_vpc.this.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
  availability_zone = var.availability_zones[count.index]

  tags = merge(local.tags_comuns, {
    Name = "${var.project_name}-${var.environment}-subnet-priv-${count.index + 1}"
    Tier = "private"
  })
}

resource "aws_eip" "nat" {
  count  = local.qtd_nat_gateways
  domain = "vpc"

  tags = merge(local.tags_comuns, {
    Name = "${var.project_name}-${var.environment}-eip-nat-${count.index + 1}"
  })

  depends_on = [aws_internet_gateway.this]
}

resource "aws_nat_gateway" "this" {
  count = local.qtd_nat_gateways

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.publica[count.index].id

  tags = merge(local.tags_comuns, {
    Name = "${var.project_name}-${var.environment}-nat-${count.index + 1}"
  })

  depends_on = [aws_internet_gateway.this]
}

resource "aws_route_table" "publica" {
  vpc_id = aws_vpc.this.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.this.id
  }

  tags = merge(local.tags_comuns, {
    Name = "${var.project_name}-${var.environment}-rt-publica"
  })
}

resource "aws_route_table" "privada" {
  count  = length(var.availability_zones)
  vpc_id = aws_vpc.this.id

  dynamic "route" {
    for_each = local.qtd_nat_gateways > 0 ? [1] : []
    content {
      cidr_block     = "0.0.0.0/0"
      nat_gateway_id = aws_nat_gateway.this[
        min(count.index, local.qtd_nat_gateways - 1)
      ].id
    }
  }

  tags = merge(local.tags_comuns, {
    Name = "${var.project_name}-${var.environment}-rt-privada-${count.index + 1}"
  })
}

resource "aws_route_table_association" "publica" {
  count          = length(var.availability_zones)
  subnet_id      = aws_subnet.publica[count.index].id
  route_table_id = aws_route_table.publica.id
}

resource "aws_route_table_association" "privada" {
  count          = length(var.availability_zones)
  subnet_id      = aws_subnet.privada[count.index].id
  route_table_id = aws_route_table.privada[count.index].id
}
# modules/vpc/outputs.tf

output "vpc_id" {
  description = "ID da VPC criada"
  value       = aws_vpc.this.id
}

output "vpc_cidr" {
  description = "Bloco CIDR da VPC"
  value       = aws_vpc.this.cidr_block
}

output "ids_subnets_publicas" {
  description = "Lista de IDs das subnets públicas"
  value       = aws_subnet.publica[*].id
}

output "ids_subnets_privadas" {
  description = "Lista de IDs das subnets privadas"
  value       = aws_subnet.privada[*].id
}

output "ids_nat_gateways" {
  description = "Lista de IDs dos NAT Gateways criados"
  value       = aws_nat_gateway.this[*].id
}

output "id_internet_gateway" {
  description = "ID do Internet Gateway"
  value       = aws_internet_gateway.this.id
}

Criando um Módulo de RDS

# modules/rds/variables.tf

variable "project_name" {
  type = string
}

variable "environment" {
  type = string
}

variable "vpc_id" {
  description = "ID da VPC onde o banco será criado"
  type        = string
}

variable "subnet_ids" {
  description = "IDs das subnets para o subnet group do RDS"
  type        = list(string)
}

variable "security_group_ids_permitidos" {
  description = "IDs dos security groups com permissão de acesso ao banco"
  type        = list(string)
}

variable "engine_version" {
  description = "Versão do PostgreSQL"
  type        = string
  default     = "16.1"
}

variable "instance_class" {
  description = "Classe da instância RDS"
  type        = string
  default     = "db.t3.micro"
}

variable "allocated_storage" {
  description = "Tamanho inicial do storage em GB"
  type        = number
  default     = 20
}

variable "db_name" {
  description = "Nome do banco de dados inicial"
  type        = string
}

variable "db_username" {
  description = "Username do banco de dados"
  type        = string
  sensitive   = true
}

variable "db_password" {
  description = "Senha do banco de dados"
  type        = string
  sensitive   = true
}

variable "backup_retention_days" {
  description = "Dias de retenção dos backups automáticos"
  type        = number
  default     = 7
}

variable "multi_az" {
  description = "Habilita Multi-AZ para alta disponibilidade"
  type        = bool
  default     = false
}

variable "deletion_protection" {
  description = "Habilita proteção contra exclusão acidental"
  type        = bool
  default     = true
}
# modules/rds/main.tf

resource "aws_security_group" "rds" {
  name        = "${var.project_name}-${var.environment}-sg-rds"
  description = "Security group do RDS PostgreSQL"
  vpc_id      = var.vpc_id

  ingress {
    description     = "PostgreSQL dos security groups permitidos"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = var.security_group_ids_permitidos
  }

  tags = {
    Name        = "${var.project_name}-${var.environment}-sg-rds"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

resource "aws_db_subnet_group" "this" {
  name       = "${var.project_name}-${var.environment}-db-subnet-group"
  subnet_ids = var.subnet_ids

  tags = {
    Name        = "${var.project_name}-${var.environment}-db-subnet-group"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

resource "aws_db_parameter_group" "this" {
  name   = "${var.project_name}-${var.environment}-pg16"
  family = "postgres16"

  parameter {
    name  = "log_min_duration_statement"
    value = "1000"
  }

  parameter {
    name  = "log_connections"
    value = "1"
  }

  lifecycle {
    create_before_destroy = true
  }

  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

resource "aws_db_instance" "this" {
  identifier = "${var.project_name}-${var.environment}-db"

  engine         = "postgres"
  engine_version = var.engine_version
  instance_class = var.instance_class

  allocated_storage     = var.allocated_storage
  max_allocated_storage = var.allocated_storage * 5
  storage_type          = "gp3"
  storage_encrypted     = true

  db_name  = var.db_name
  username = var.db_username
  password = var.db_password

  db_subnet_group_name   = aws_db_subnet_group.this.name
  parameter_group_name   = aws_db_parameter_group.this.name
  vpc_security_group_ids = [aws_security_group.rds.id]

  multi_az            = var.multi_az
  publicly_accessible = false
  deletion_protection = var.deletion_protection

  backup_retention_period   = var.backup_retention_days
  backup_window             = "03:00-04:00"
  maintenance_window        = "Mon:04:00-Mon:05:00"
  skip_final_snapshot       = !var.deletion_protection
  final_snapshot_identifier = var.deletion_protection ? "${var.project_name}-${var.environment}-final" : null

  performance_insights_enabled = true

  tags = {
    Name        = "${var.project_name}-${var.environment}-db"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}
# modules/rds/outputs.tf

output "endpoint" {
  description = "Endpoint de conexão do banco de dados"
  value       = aws_db_instance.this.endpoint
  sensitive   = true
}

output "porta" {
  description = "Porta do banco de dados"
  value       = aws_db_instance.this.port
}

output "nome_banco" {
  description = "Nome do banco de dados criado"
  value       = aws_db_instance.this.db_name
}

output "id_instancia" {
  description = "ID da instância RDS"
  value       = aws_db_instance.this.id
}

output "id_security_group" {
  description = "ID do security group do RDS"
  value       = aws_security_group.rds.id
}

output "database_url" {
  description = "URL de conexão completa"
  value = format(
    "postgresql://%s:%s@%s/%s",
    var.db_username,
    var.db_password,
    aws_db_instance.this.endpoint,
    var.db_name
  )
  sensitive = true
}

Usando os Módulos em um Projeto Raiz

Com os módulos criados, o código do projeto raiz torna-se significativamente mais limpo e legível:

# environments/staging/main.tf

terraform {
  required_version = ">= 1.7.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.31"
    }
  }

  backend "s3" {
    bucket = "minha-empresa-terraform-state"
    key    = "staging/terraform.tfstate"
    region = "us-east-1"
  }
}

provider "aws" {
  region = var.aws_region
}

# ── Módulo de VPC ─────────────────────────────────
module "vpc" {
  source = "../../modules/vpc"

  project_name       = var.project_name
  environment        = "staging"
  vpc_cidr           = "10.1.0.0/16"
  availability_zones = ["us-east-1a", "us-east-1b"]

  # Staging usa apenas um NAT Gateway para reduzir custos
  nat_gateway_por_az = false

  tags_adicionais = {
    CostCenter = "engenharia"
  }
}

# Security group da aplicação para referenciar no RDS
resource "aws_security_group" "aplicacao" {
  name   = "${var.project_name}-staging-sg-app"
  vpc_id = module.vpc.vpc_id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# ── Módulo de RDS ─────────────────────────────────
module "rds" {
  source = "../../modules/rds"

  project_name = var.project_name
  environment  = "staging"
  vpc_id       = module.vpc.vpc_id
  subnet_ids   = module.vpc.ids_subnets_privadas

  security_group_ids_permitidos = [aws_security_group.aplicacao.id]

  instance_class      = "db.t3.micro"
  allocated_storage   = 20
  db_name             = "staging_db"
  db_username         = var.db_username
  db_password         = var.db_password
  multi_az            = false
  deletion_protection = false
  backup_retention_days = 1
}

O mesmo projeto de produção usa os mesmos módulos com configurações diferentes — sem duplicar nenhuma lógica:

# environments/production/main.tf

module "vpc" {
  source = "../../modules/vpc"

  project_name       = var.project_name
  environment        = "production"
  vpc_cidr           = "10.0.0.0/16"
  availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]

  # Produção usa NAT Gateway por AZ para alta disponibilidade
  nat_gateway_por_az = true
}

module "rds" {
  source = "../../modules/rds"

  project_name = var.project_name
  environment  = "production"
  vpc_id       = module.vpc.vpc_id
  subnet_ids   = module.vpc.ids_subnets_privadas

  security_group_ids_permitidos = [aws_security_group.aplicacao.id]

  instance_class        = "db.r6g.large"
  allocated_storage     = 100
  db_name               = "producao_db"
  db_username           = var.db_username
  db_password           = var.db_password
  multi_az              = true
  deletion_protection   = true
  backup_retention_days = 7
}

Módulos Públicos do Terraform Registry

O Terraform Registry hospeda módulos públicos e gratuitos mantidos pela comunidade e pelos próprios provedores. Em vez de escrever um módulo de VPC do zero, é possível usar o módulo oficial mantido pela própria HashiCorp:

# Usando o módulo oficial de VPC para AWS
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.2"

  name = "${var.project_name}-${var.environment}"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway     = true
  single_nat_gateway     = false
  one_nat_gateway_per_az = true

  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

A decisão entre usar módulos públicos e escrever os próprios depende do contexto. Módulos públicos bem mantidos — como os da terraform-aws-modules — incorporam anos de boas práticas e cobrem casos extremos que seriam difíceis de antecipar. A desvantagem é a dependência de um projeto externo. Para infraestrutura crítica de produção, muitas organizações preferem manter módulos internos com controle total sobre cada decisão.


Testando Módulos com Terratest

Módulos críticos merecem testes automatizados — assim como qualquer outro código de produção. O Terratest é uma biblioteca Go que permite escrever testes que provisionam infraestrutura real, verificam seu comportamento e a destroem ao final:

// modules/vpc/test/vpc_test.go
package test

import (
  "testing"

  "github.com/gruntwork-io/terratest/modules/terraform"
  "github.com/stretchr/testify/assert"
)

func TestVPCModule(t *testing.T) {
  t.Parallel()

  terraformOptions := &terraform.Options{
    TerraformDir: "../",
    Vars: map[string]interface{}{
      "project_name":       "test",
      "environment":        "development",
      "vpc_cidr":           "10.99.0.0/16",
      "availability_zones": []string{"us-east-1a", "us-east-1b"},
      "habilitar_nat_gateway": false,
    },
  }

  // Garante que a infraestrutura será destruída ao final
  defer terraform.Destroy(t, terraformOptions)

  // Provisiona a infraestrutura
  terraform.InitAndApply(t, terraformOptions)

  // Verifica os outputs
  vpcID := terraform.Output(t, terraformOptions, "vpc_id")
  assert.NotEmpty(t, vpcID)

  subnetsPubs := terraform.OutputList(t, terraformOptions, "ids_subnets_publicas")
  assert.Equal(t, 2, len(subnetsPubs))

  subnetsPrivs := terraform.OutputList(t, terraformOptions, "ids_subnets_privadas")
  assert.Equal(t, 2, len(subnetsPrivs))
}

Organização de Módulos em Projetos Grandes

Em organizações com múltiplos times e dezenas de projetos, a estrutura recomendada separa módulos em um repositório dedicado:

infraestrutura/
├── modules/                    # Repositório separado ou diretório central
│   ├── vpc/
│   ├── rds/
│   ├── ecs-service/
│   ├── alb/
│   ├── s3-bucket/
│   └── cloudfront/
│
├── environments/
│   ├── development/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   ├── staging/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── production/
│       ├── main.tf
│       ├── variables.tf
│       └── terraform.tfvars
│
└── shared/                     # Recursos compartilhados entre ambientes
    ├── ecr/                    # Registro de containers
    ├── route53/                # DNS
    └── acm/                    # Certificados SSL

Quando os módulos estão em um repositório separado, referenciados com versão específica:

module "vpc" {
  source  = "git::https://github.com/minha-empresa/terraform-modules.git//vpc?ref=v2.3.1"

  project_name       = var.project_name
  environment        = var.environment
  vpc_cidr           = var.vpc_cidr
  availability_zones = var.availability_zones
}

O uso de ?ref=v2.3.1 garante que o módulo é fixado em uma versão específica — mudanças no repositório de módulos não afetam projetos existentes sem uma atualização deliberada.


O Que Vem a Seguir

O próximo artigo aborda o state do Terraform — o componente mais crítico e frequentemente menos compreendido de toda a ferramenta. Entender como o state funciona, como armazená-lo com segurança em um backend remoto e como manipulá-lo quando as coisas saem do esperado é uma habilidade essencial para qualquer engenheiro que trabalha com Terraform em equipe.


Referências para Aprofundamento

Documentação oficial - Terraform Modules — developer.hashicorp.com — Documentação completa sobre criação, uso e publicação de módulos Terraform, incluindo boas práticas de estrutura e versionamento. - Terraform Registry — registry.terraform.io — Catálogo de módulos e providers públicos do ecossistema Terraform. Inclui módulos para AWS, Azure, GCP e dezenas de outros provedores.

Módulos de referência - terraform-aws-modules — GitHub — Organização no GitHub com módulos oficialmente mantidos para os serviços AWS mais usados. O código-fonte é uma excelente referência de boas práticas.

Testes - Terratest — GitHub — Biblioteca Go para testes automatizados de infraestrutura Terraform. Documentação completa com exemplos para os principais serviços AWS. - Gruntwork — Testing Infrastructure as Code — Guia de início rápido do Terratest com exemplos práticos de testes de módulos.


Artigo 27 de 52 · Módulo 5 — Infraestrutura como Código Prof. Ricardo Matos · Série Dominando DevOps & Cloud em 1 Ano


you asked

Continue


claude response

Comentários

Mais em DevOps

Performance e FinOps: Otimizando Custo e Velocidade na Cloud
Performance e FinOps: Otimizando Custo e Velocidade na Cloud

Existe uma correlação quase universal entre o crescimento de um sistema em pr...

Docker Compose: Orquestrando Múltiplos Serviços Localmente
Docker Compose: Orquestrando Múltiplos Serviços Localmente

O artigo anterior terminou com um conjunto de comandos docker run que co...

Introdução ao Terraform: Infraestrutura que Você Pode Versionar
Introdução ao Terraform: Infraestrutura que Você Pode Versionar

Imagine a seguinte situação, comum em empresas que não adotaram Infraestrutur...