DevOps

Terraform: Infrastructure as Code na prática com AWS

Atualizado em Março 2026

Terraform é uma ferramenta open-source de Infrastructure as Code (IaC) criada pela HashiCorp que permite definir, provisionar e gerenciar toda a infraestrutura de nuvem usando código declarativo. Com suporte a AWS, Azure, GCP e centenas de provedores, o Terraform se tornou o padrão de mercado para automação de infraestrutura em 2026.

Provisionar infraestrutura manualmente pelo console da AWS é rápido para testes, mas se torna insustentável à medida que o ambiente cresce. Mudanças não rastreadas, configurações inconsistentes entre ambientes e a impossibilidade de reproduzir a infraestrutura de forma confiável são problemas comuns que afetam equipes de todos os tamanhos.

O Terraform, da HashiCorp, resolve esses problemas ao permitir que você defina toda a sua infraestrutura em código declarativo (HCL, HashiCorp Configuration Language). Neste guia, vamos do zero ao deploy de uma infraestrutura completa na AWS, cobrindo módulos, state management e as melhores práticas que usamos nos projetos da RFX Tecnologia.

Por que Terraform?

Existem diversas ferramentas de IaC no mercado: CloudFormation (nativo AWS), Pulumi, CDK, Ansible, entre outras. O Terraform se destaca por alguns motivos, sendo inclusive essencial para estratégias multi-cloud:

  • Multi-cloud: funciona com AWS, Azure, GCP, Kubernetes e centenas de outros provedores com a mesma linguagem
  • Declarativo: você descreve o estado desejado e o Terraform calcula as mudanças necessárias
  • Plan antes de Apply: o comando terraform plan mostra exatamente o que será alterado antes de executar
  • State management: mantém um mapa do estado atual da infraestrutura para calcular diffs precisos
  • Ecossistema maduro: o Terraform Registry possui milhares de módulos prontos e provedores oficiais
  • Comunidade ativa: ampla documentação, tutoriais e suporte da comunidade

Estrutura de projeto recomendada

Uma boa organização de código é fundamental para projetos Terraform escaláveis. Recomendamos a seguinte estrutura:

estrutura-projeto
infraestrutura/
├── modules/
│   ├── vpc/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── ecs/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── rds/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   ├── staging/
│   │   └── ...
│   └── prod/
│       └── ...
└── README.md

Cada módulo encapsula um componente de infraestrutura reutilizável (VPC, cluster ECS, banco RDS, etc.). Os ambientes (dev, staging, prod) consomem esses módulos com configurações específicas via terraform.tfvars.

Exemplo prático: VPC + Subnets + Security Groups

Vamos criar uma VPC completa com subnets públicas e privadas, NAT Gateway e Security Groups. Este é o alicerce de qualquer infraestrutura na AWS:

modules/vpc/main.tf
# Módulo VPC com subnets públicas e privadas

terraform {
  required_version = ">= 1.7.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

variable "project_name" {
  type        = string
  description = "Nome do projeto para tagging"
}

variable "environment" {
  type        = string
  description = "Ambiente (dev, staging, prod)"
}

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

variable "availability_zones" {
  type        = list(string)
  default     = ["sa-east-1a", "sa-east-1b", "sa-east-1c"]
  description = "Availability Zones para as subnets"
}

# VPC Principal
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

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

# Subnets Públicas
resource "aws_subnet" "public" {
  count                   = length(var.availability_zones)
  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name        = "${var.project_name}-${var.environment}-public-${count.index + 1}"
    Environment = var.environment
    Type        = "public"
  }
}

# Subnets Privadas
resource "aws_subnet" "private" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name        = "${var.project_name}-${var.environment}-private-${count.index + 1}"
    Environment = var.environment
    Type        = "private"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name        = "${var.project_name}-${var.environment}-igw"
    Environment = var.environment
  }
}

# Elastic IP para NAT Gateway
resource "aws_eip" "nat" {
  domain = "vpc"

  tags = {
    Name        = "${var.project_name}-${var.environment}-nat-eip"
    Environment = var.environment
  }
}

# NAT Gateway (em subnet pública)
resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public[0].id

  tags = {
    Name        = "${var.project_name}-${var.environment}-nat"
    Environment = var.environment
  }

  depends_on = [aws_internet_gateway.main]
}

# Route Table - Pública
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

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

  tags = {
    Name = "${var.project_name}-${var.environment}-public-rt"
  }
}

# Route Table - Privada
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main.id
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-private-rt"
  }
}

# Outputs
output "vpc_id" {
  value = aws_vpc.main.id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  value = aws_subnet.private[*].id
}

Gerenciamento de State

O state file do Terraform (terraform.tfstate) armazena o mapeamento entre os recursos definidos no código e os recursos reais na AWS. Este arquivo é crítico: se for perdido ou corrompido, o Terraform perde a referência dos recursos existentes.

Nunca armazene o state localmente em projetos de equipe. Use um remote backend com S3 + DynamoDB para locking:

environments/prod/backend.tf
terraform {
  backend "s3" {
    bucket         = "rfx-terraform-state-prod"
    key            = "prod/terraform.tfstate"
    region         = "sa-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}

# O bucket S3 e a tabela DynamoDB devem ser criados
# previamente (bootstrap) com criptografia e versionamento.

O DynamoDB table fornece state locking, impedindo que duas execuções simultâneas de terraform apply corrompam o state. O bucket S3 com versionamento permite recuperar versões anteriores do state em caso de problemas.

Fluxo de trabalho com Terraform

O fluxo de trabalho padrão do Terraform segue quatro etapas:

terminal
# 1. Inicializar o projeto (baixar providers e configurar backend)
terraform init

# 2. Validar a sintaxe é a configuração
terraform validate

# 3. Visualizar as mudanças planejadas (SEMPRE faça isso antes do apply)
terraform plan -out=tfplan

# 4. Aplicar as mudanças (com o plano salvo para garantir consistência)
terraform apply tfplan

# Extras úteis:
# Formatar o código automaticamente
terraform fmt -recursive

# Visualizar o state atual
terraform state list

# Importar recurso existente para o state
terraform import aws_s3_bucket.existing bucket-name

# Destruir toda a infraestrutura (CUIDADO!)
terraform destroy

Melhores práticas de Terraform na AWS

1. Use variáveis e locals

Nunca hardcode valores. Use variables.tf para inputs e locals para valores computados. Isso torna o código reutilizável entre ambientes.

2. Implemente tagging consistente

Defina tags padrão usando default_tags no provider para garantir que todos os recursos sejam tagueados automaticamente:

provider.tf
provider "aws" {
  region = "sa-east-1"

  default_tags {
    tags = {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "terraform"
      Team        = "platform"
      CostCenter  = var.cost_center
    }
  }
}

3. Use data sources em vez de hardcode

Em vez de hardcodar AMI IDs ou ARNs, use data sources para buscá-los dinamicamente. Isso garante que o código funcione em diferentes contas e regiões.

4. Implemente políticas com Sentinel ou OPA

Para ambientes corporativos, use Sentinel (Terraform Cloud/Enterprise) ou OPA (Open Policy Agent) com conftest para validar automaticamente que o código Terraform segue as políticas de segurança e compliance da empresa antes do apply.

5. Automatize com CI/CD

Nunca execute terraform apply manualmente em produção. Integre o Terraform ao seu pipeline de CI/CD, como mostramos no artigo sobre CI/CD com GitHub Actions. com aprovação manual para ambiente de produção. Armazene o plano como artefato e aplique exatamente o plano aprovado.

6. Versione módulos

Ao referenciar módulos internos ou do Registry, sempre especifique a versão. Isso evita que atualizações de módulos causem mudanças inesperadas na infraestrutura.

"Infrastructure as Code não é apenas sobre automação, é sobre criar uma fonte de verdade confiável e auditável para toda a sua infraestrutura. Se não está no código, não deveria existir."

Gotchas e armadilhas comuns do Terraform na AWS

Problemas que encontramos com frequencia em projetos reais:

  • State lock não configurado: sem DynamoDB para locking, duas pessoas rodando terraform apply simultaneamente podem corromper o state. Isso acontece mais do que se imagina em times sem CI/CD.
  • Usar terraform destroy sem filtros: em ambientes compartilhados, um destroy acidental pode derrubar toda a infraestrutura. Use -target para destruir recursos específicos e implemente prevent_destroy = true em recursos críticos como RDS e S3.
  • Provider version sem constraints: não travar a versão do provider AWS pode causar breaking changes inesperadas. Sempre use version = "~> 5.0" (a partir do Terraform 1.6+, o lock file .terraform.lock.hcl ajuda, mas o constraint explícito é essencial).
  • Secrets em terraform.tfvars: nunca coloque senhas ou tokens em arquivos tfvars commitados no Git. Use data "aws_secretsmanager_secret_version" ou variáveis de ambiente TF_VAR_ injetadas pelo CI/CD.
  • Módulos sem versão: referenciar módulos com source = "../modules/vpc" sem tag de versão significa que qualquer alteração no módulo afeta todos os ambientes. Use Git tags: source = "git::https://github.com/org/modules.git//vpc?ref=v1.2.0".
lifecycle-e-seguranca.tf
# Proteger recursos críticos contra destroy acidental
resource "aws_db_instance" "production" {
  identifier     = "app-producao"
  engine         = "postgres"
  engine_version = "16.1"
  instance_class = "db.r6g.large"
  # ...

  lifecycle {
    prevent_destroy = true
    ignore_changes  = [engine_version] # Evitar redeploy em minor updates
  }
}

# Usar data source para secrets (nunca hardcode)
data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "prod/database/master-password"
}

resource "aws_db_instance" "example" {
  password = data.aws_secretsmanager_secret_version.db_password.secret_string
  # ...
}

# Moved blocks (Terraform 1.1+) para refatorar sem destruir
moved {
  from = aws_instance.web_server
  to   = module.compute.aws_instance.web_server
}

# Import blocks (Terraform 1.5+) para importar recursos existentes
import {
  to = aws_s3_bucket.existing_bucket
  id = "meu-bucket-legado"
}

Na Prática: migração de ClickOps para IaC

Uma empresa de logística com 80 recursos AWS criados manualmente pelo console (o famoso "ClickOps") enfrentava problemas recorrentes: ambientes de staging e produção com configurações divergentes, impossibilidade de recriar o ambiente em caso de desastre, e deploys manuais que consumiam 2 dias por release.

  • Semana 1-2: mapeamos todos os recursos existentes com aws configservice get-discovered-resource-counts e usamos o Terraformer para gerar o código inicial. Revisamos e refatoramos em módulos.
  • Semana 3: importamos os recursos para o state com terraform import (no Terraform 1.5+, usamos import blocks para tornar o processo declarativo e auditável).
  • Semana 4: configuramos o pipeline de CI/CD no GitHub Actions com terraform plan no PR e terraform apply automático após merge na main, com aprovação manual para produção.

Resultado: tempo de provisionamento de ambiente caiu de 2 dias para 15 minutos. Drift entre staging e produção foi eliminado. A equipe agora faz 3x mais deploys por semana com confiança.

KPIs para o Decisor

  • Tempo de provisionamento de ambiente: de request até ambiente funcional. Target: menos de 30 minutos com IaC vs. dias no modo manual.
  • Drift detection rate (%): percentual de recursos com configuração divergente do código. Target: 0% (use terraform plan em schedule para detectar drift).
  • Cobertura IaC (%): percentual de recursos AWS gerenciados por Terraform. Target: acima de 95% em 6 meses.
  • Mean time to recovery (MTTR): tempo para recriar infraestrutura do zero. Target: menos de 1 hora com IaC completo.
  • Change failure rate (%): percentual de applies que causam rollback. Target: abaixo de 5% com plan + review em PR.

Terraform vs CloudFormation: quando usar cada um

Para ambientes exclusivamente AWS, o CloudFormation tem a vantagem de ser nativo e integrado com todos os serviços AWS no lançamento. Porém, o Terraform oferece uma linguagem mais expressiva (HCL vs JSON/YAML), melhor experiência de desenvolvimento (plan visual, módulos, providers) e portabilidade multi-cloud.

Na RFX Tecnologia, recomendamos Terraform como padrão para a maioria dos cenários. Usamos CloudFormation apenas quando há recursos que ainda não possuem provider Terraform ou em integrações específicas como Service Catalog.

Próximos passos

Se você está começando com Terraform, siga esta sequência: crie um projeto simples com uma VPC, evolua para módulos, configure remote state, e por fim integre com CI/CD. A curva de aprendizado é suave e os benefícios são imediatos.

Na RFX Tecnologia, por meio do nosso serviço de DevOps e Automação, ajudamos empresas a implementar IaC com Terraform desde o zero. Definimos a estrutura de módulos, configuramos pipelines de CI/CD e treinamos equipes para operar com autonomia. Fale conosco para saber mais.

Fale Conosco
Assessment gratuito do seu ambiente AWS