Terraform é uma ferramenta de Infrastructure as Code (IaC) criada pela HashiCorp, com código-fonte disponível sob a Business Source License (BSL) 1.1, adotada pela HashiCorp em 10/08/2023 para as versões seguintes dos seus produtos (abre em nova aba) (atualização de 22/10/2026). Ele 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 (abre em nova aba) 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.10.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.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 no S3, com o lock nativo do próprio S3:

environments/prod/backend.tf
terraform {
  backend "s3" {
    bucket         = "rfx-terraform-state-prod"
    key            = "prod/terraform.tfstate"
    region         = "sa-east-1"
    encrypt        = true
    use_lockfile   = true  # lock nativo no S3 (Terraform 1.10+)
  }
}

# O bucket S3 deve ser criado previamente (bootstrap),
# com criptografia e versionamento.

O use_lockfile grava um arquivo de lock ao lado do state no S3 e 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. O lock com tabela do DynamoDB, comum em projetos antigos, está depreciado na documentação do backend S3 e será removido numa versão futura; quem ainda usa pode configurar os dois durante a migração.

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 (abre em nova aba) 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 frequentes em projetos reais:

  • State lock não configurado: sem lock no backend, 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 (abre em nova aba) 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. Trave a versão major (abre em nova aba), por exemplo version = "~> 6.0" (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: cenário ilustrativo de migração de ClickOps para IaC

Cenário ilustrativo: uma empresa de logística tem cerca de 80 recursos AWS criados à mão pelo console (o famoso "ClickOps") e convive com problemas recorrentes: staging e produção com configurações divergentes, nenhuma forma de recriar o ambiente em caso de desastre e deploys manuais que tomam dias a cada release. O desenho para esse cenário segue três etapas:

  • Etapa 1: mapear os recursos existentes com aws configservice get-discovered-resource-counts e gerar o código inicial com o Terraformer, revisado e refatorado em módulos.
  • Etapa 2: importar os recursos para o state com terraform import (a partir do Terraform 1.5, com import blocks (abre em nova aba), o que deixa o processo declarativo e auditável).
  • Etapa 3: pipeline de CI/CD no GitHub Actions com terraform plan no PR e terraform apply automático após o merge na main, com aprovação manual para produção.

O que acompanhar nesse cenário: o tempo para provisionar um ambiente, que passa a ser o tempo de um terraform apply; o drift entre staging e produção, que o terraform plan agendado expõe; e a cobertura de IaC, com a meta de ter praticamente todos os recursos gerenciados por código.

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.

O catálogo de projetos traz um exemplo de escopo de ambiente base na AWS (landing zone) com governança e backup.

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.

Documentação oficial consultada em 13/10/2026: backend S3 do Terraform (HashiCorp) e provider AWS do Terraform (HashiCorp).

Falar com um engenheiro