DevOps Admin

O que Todo Dev Deve Saber sobre Terraform Avançado: Módulos, Workspaces e Remote State Já leu

Entendendo Módulos no Terraform Um módulo no Terraform é um diretório contendo um conjunto de arquivos de configuração que funcionam juntos. Ele encapsula lógica de infraestrutura reutilizável, permitindo que você organize código complexo em componentes menores e gerenciáveis. A verdadeira potência dos módulos está na abstração: você define a interface (inputs e outputs) e a implementação fica encapsulada, possibilitando reutilização sem duplicação de código. A estrutura básica de um módulo segue um padrão simples. Imagine que você quer criar um módulo para provisionar uma instância EC2 com segurança básica. Você criará um diretório chamado com arquivos de configuração. O módulo precisa expor inputs via variáveis e outputs para que o código chamador acesse informações importantes, como o ID da instância ou o IP público. Estrutura e Organização de Módulos Exemplo Prático: Módulo EC2 Vamos criar um módulo reutilizável para provisionar uma instância EC2. Primeiro, o arquivo de variáveis do módulo ( ): Agora a implementação ( ): E os outputs

Entendendo Módulos no Terraform

Um módulo no Terraform é um diretório contendo um conjunto de arquivos de configuração que funcionam juntos. Ele encapsula lógica de infraestrutura reutilizável, permitindo que você organize código complexo em componentes menores e gerenciáveis. A verdadeira potência dos módulos está na abstração: você define a interface (inputs e outputs) e a implementação fica encapsulada, possibilitando reutilização sem duplicação de código.

A estrutura básica de um módulo segue um padrão simples. Imagine que você quer criar um módulo para provisionar uma instância EC2 com segurança básica. Você criará um diretório chamado modules/ec2 com arquivos de configuração. O módulo precisa expor inputs via variáveis e outputs para que o código chamador acesse informações importantes, como o ID da instância ou o IP público.

Estrutura e Organização de Módulos

projeto-terraform/
├── main.tf
├── variables.tf
├── outputs.tf
├── terraform.tfvars
└── modules/
    └── ec2_instance/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

Exemplo Prático: Módulo EC2

Vamos criar um módulo reutilizável para provisionar uma instância EC2. Primeiro, o arquivo de variáveis do módulo (modules/ec2_instance/variables.tf):

variable "instance_name" {
  description = "Nome da instância EC2"
  type        = string
}

variable "instance_type" {
  description = "Tipo da instância"
  type        = string
  default     = "t3.micro"
}

variable "ami_id" {
  description = "ID da AMI a usar"
  type        = string
}

variable "security_group_id" {
  description = "ID do grupo de segurança"
  type        = string
}

variable "tags" {
  description = "Tags para a instância"
  type        = map(string)
  default     = {}
}

Agora a implementação (modules/ec2_instance/main.tf):

resource "aws_instance" "this" {
  ami                    = var.ami_id
  instance_type          = var.instance_type
  vpc_security_group_ids = [var.security_group_id]

  tags = merge(
    var.tags,
    {
      Name = var.instance_name
    }
  )
}

E os outputs do módulo (modules/ec2_instance/outputs.tf):

output "instance_id" {
  description = "ID da instância"
  value       = aws_instance.this.id
}

output "public_ip" {
  description = "IP público da instância"
  value       = aws_instance.this.public_ip
}

output "private_ip" {
  description = "IP privado da instância"
  value       = aws_instance.this.private_ip
}

Agora, para usar este módulo na configuração principal (main.tf):

module "app_server" {
  source = "./modules/ec2_instance"

  instance_name      = "servidor-aplicacao"
  instance_type      = "t3.small"
  ami_id             = "ami-0c55b159cbfafe1f0"
  security_group_id  = aws_security_group.app.id

  tags = {
    Environment = "production"
    Project     = "meu-projeto"
  }
}

output "app_server_ip" {
  value = module.app_server.public_ip
}

A vantagem aqui é clara: você pode chamar o módulo múltiplas vezes com diferentes parâmetros, e toda a lógica está centralizada. Se precisar ajustar como as instâncias são criadas, faz apenas uma vez no módulo.


Workspaces: Gerenciando Múltiplos Ambientes

Workspaces no Terraform permitem manter múltiplos estados separados dentro da mesma configuração. Eles são úteis quando você precisa de ambientes distintos (desenvolvimento, staging, produção) usando a mesma lógica de código. Sem workspaces, você precisaria duplicar toda a configuração ou usar variáveis complexas para diferenciar ambientes — workspaces oferecem uma solução elegante.

Cada workspace tem seu próprio arquivo de estado (.tfstate), isolando completamente a infraestrutura de um ambiente. Quando você alterna entre workspaces, o Terraform carrega o estado correspondente, aplicando as mudanças apenas àquele ambiente. Isso é particularmente poderoso porque sua configuração permanece idêntica; apenas os valores das variáveis mudam por ambiente.

Funcionamento Básico de Workspaces

Por padrão, o Terraform começa com um workspace chamado default. Você pode criar novos workspaces e alternar entre eles. Vamos criar um exemplo prático onde deployamos a mesma infraestrutura em diferentes ambientes.

# variables.tf
variable "environment" {
  description = "Ambiente de deployment"
  type        = string
  default     = terraform.workspace
}

variable "instance_count" {
  description = "Número de instâncias por ambiente"
  type        = number

  default = (
    terraform.workspace == "production" ? 3 :
    terraform.workspace == "staging" ? 2 : 1
  )
}

variable "instance_type" {
  description = "Tipo de instância por ambiente"
  type        = string

  default = (
    terraform.workspace == "production" ? "t3.large" :
    terraform.workspace == "staging" ? "t3.medium" : "t3.micro"
  )
}
# main.tf
resource "aws_instance" "web" {
  count           = var.instance_count
  ami             = "ami-0c55b159cbfafe1f0"
  instance_type   = var.instance_type

  tags = {
    Name        = "web-server-${terraform.workspace}-${count.index + 1}"
    Environment = var.environment
  }
}

Para usar workspaces na prática:

# Listar workspaces existentes
terraform workspace list

# Criar novo workspace
terraform workspace new production
terraform workspace new staging

# Alternar para um workspace
terraform workspace select production

# Verificar workspace atual
terraform workspace show

# Aplicar configuração no workspace atual
terraform apply

# Voltar para default
terraform workspace select default

Observe que o estado é mantido separado. Quando você faz terraform apply no workspace production, o arquivo de estado é específico daquele workspace (geralmente armazenado como terraform.tfstate.d/production/terraform.tfstate). Isso significa que você pode ter 3 instâncias em produção, 2 em staging e 1 em desenvolvimento, tudo gerenciado pelo mesmo código.


Remote State: Compartilhando Estado entre Equipes

Remote state resolve o maior problema do Terraform em equipes: o estado não fica mais em um arquivo local. Quando múltiplas pessoas trabalham no mesmo projeto, alguém precisa gerenciar a versão "correta" do estado. Remote state armazena esse estado em um backend centralizado (como AWS S3, Terraform Cloud, ou Azure Storage), permitindo que toda a equipe trabalhe sobre a mesma versão.

Além de compartilhamento, remote state oferece locking automático — quando alguém está aplicando mudanças, outros são impedidos de fazer o mesmo simultaneamente, evitando corrupção do estado. Também fornece melhor segurança: você pode usar encryption em repouso e em trânsito, além de controlar acesso via políticas de permissão.

Configurando Remote State com AWS S3

O backend mais comum é AWS S3 com DynamoDB para locking. Você precisa criar esses recursos previamente (uma única vez):

# Arquivo separado: backends.tf (criar manualmente na AWS ou via outro Terraform)
resource "aws_s3_bucket" "terraform_state" {
  bucket = "meu-projeto-terraform-state"
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_dynamodb_table" "terraform_locks" {
  name           = "terraform-locks"
  billing_mode   = "PAY_PER_REQUEST"
  hash_key       = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Após criar S3 e DynamoDB, configure o backend no seu projeto Terraform (backend.tf):

terraform {
  backend "s3" {
    bucket         = "meu-projeto-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Quando você executa terraform init com essa configuração, o Terraform migra o estado local para S3:

terraform init
# Output: Backend has been successfully initialized!

Integrando Remote State com Workspaces

Remote state funciona perfeitamente com workspaces. Você pode organizar o estado por ambiente usando diferentes chaves S3:

# backend.tf com dynamic path
terraform {
  backend "s3" {
    bucket         = "meu-projeto-terraform-state"
    key            = "${terraform.workspace}/terraform.tfstate"  # NOTA: isso não funciona literalmente
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Na realidade, você precisa usar -backend-config durante o init ou arquivo separado de configuração:

# Inicializar com configuração dinâmica
terraform init -backend-config="key=dev/terraform.tfstate" -backend-config="bucket=meu-projeto-terraform-state"

Ou use arquivo de configuração backend por workspace:

# backend-dev.tf
terraform {
  backend "s3" {
    bucket         = "meu-projeto-terraform-state"
    key            = "dev/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Acessando Outputs de Remote State em Outro Projeto

Um caso de uso poderoso é compartilhar outputs entre projetos Terraform diferentes via remote state:

# Em outro projeto que precisa do VPC ID criado em outro projeto
data "terraform_remote_state" "networking" {
  backend = "s3"

  config = {
    bucket         = "meu-projeto-terraform-state"
    key            = "networking/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
  }
}

resource "aws_security_group" "app" {
  name   = "app-sg"
  vpc_id = data.terraform_remote_state.networking.outputs.vpc_id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Padrão Integrado: Módulos + Workspaces + Remote State

A verdadeira força emerge quando você combina esses três conceitos. Vamos construir uma arquitetura realista com todos os três elementos trabalhando juntos.

Estrutura do Projeto Integrado

infraestrutura-producao/
├── terraform.tf
├── backend.tf
├── variables.tf
├── main.tf
├── terraform.tfvars
├── environments/
│   ├── dev.tfvars
│   ├── staging.tfvars
│   └── prod.tfvars
└── modules/
    ├── vpc/
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    ├── rds/
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    └── app_server/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

Backend com Isolamento por Workspace

# backend.tf
terraform {
  required_version = ">= 1.0"

  backend "s3" {
    bucket         = "meu-projeto-terraform-state"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

# Note: a chave será configurada por workspace dinamicamente

Inicializar para cada workspace com chaves diferentes:

# Development
terraform workspace new dev
terraform init -backend-config="key=dev/terraform.tfstate"

# Staging
terraform workspace new staging
terraform init -backend-config="key=staging/terraform.tfstate"

# Production
terraform workspace new prod
terraform init -backend-config="key=prod/terraform.tfstate"

Configuração Principal com Módulos

# variables.tf
variable "aws_region" {
  type = string
}

variable "vpc_cidr" {
  type = string
}

variable "database_allocated_storage" {
  type = number
}

variable "app_instance_count" {
  type = number
}

# main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

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

  cidr_block = var.vpc_cidr
  environment = terraform.workspace
}

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

  db_subnet_group_name = module.vpc.db_subnet_group_name
  allocated_storage    = var.database_allocated_storage
  environment          = terraform.workspace
}

module "app_servers" {
  source = "./modules/app_server"

  count                = var.app_instance_count
  vpc_id              = module.vpc.vpc_id
  subnet_id           = module.vpc.public_subnets[count.index % length(module.vpc.public_subnets)]
  database_endpoint   = module.rds.endpoint
  environment         = terraform.workspace
}

output "database_endpoint" {
  value = module.rds.endpoint
}

output "app_server_ips" {
  value = [for server in module.app_servers : server.public_ip]
}

Arquivos de Variáveis por Ambiente

# environments/dev.tfvars
aws_region                   = "us-east-1"
vpc_cidr                     = "10.0.0.0/16"
database_allocated_storage   = 20
app_instance_count           = 1
# environments/prod.tfvars
aws_region                   = "us-east-1"
vpc_cidr                     = "10.0.0.0/16"
database_allocated_storage   = 100
app_instance_count           = 3

Executando com Tudo Integrado

# Alternar para workspace de produção
terraform workspace select prod

# Aplicar com arquivo de variáveis específico
terraform apply -var-file="environments/prod.tfvars"

# Para desenvolvimento
terraform workspace select dev
terraform apply -var-file="environments/dev.tfvars"

# Verificar qual workspace está ativo
terraform workspace show

Conclusão

Você aprendeu que módulos encapsulam lógica reutilizável, permitindo organizar código complexo em componentes gerenciáveis que definem uma interface clara via inputs e outputs. Isso elimina duplicação e facilita manutenção centralizada da infraestrutura.

Workspaces isolam estados completamente, permitindo múltiplos ambientes (dev, staging, prod) usando o mesmo código com valores diferentes. Cada workspace mantém seu próprio estado separado, e variáveis condicionais adaptam a infraestrutura conforme o ambiente selecionado.

Remote state compartilha o estado entre equipes de forma segura, armazenando em backend centralizado (S3 + DynamoDB) com encryption e locking automático. Isso previne conflitos simultâneos e oferece auditoria de mudanças via versionamento. A integração com remote state data sources permite compartilhar outputs entre projetos diferentes, criando dependências limpas entre stacks de infraestrutura.


Referências


Artigos relacionados