Terraform para AWS: guia de provisionamento de infraestrutura
Introdução
Provisionar infraestrutura manualmente pelo console da AWS funciona quando você tem um servidor e um banco de dados. Quando o projeto cresce para múltiplos ambientes, dezenas de recursos interligados e um time de cinco pessoas fazendo deploys simultâneos, o console vira um campo minado. Você não sabe quem criou aquele Security Group aberto para o mundo, não consegue replicar o ambiente de staging em produção com confiança, e qualquer mudança vira uma operação de alto risco.
O Terraform resolve esse problema transformando sua infraestrutura em código declarativo, versionável e auditável. Neste guia, vamos cobrir o ciclo completo: da instalação à destruição de recursos, passando por exemplos práticos reais, boas práticas de segurança e os erros que mais causam dor de cabeça em times que estão adotando a ferramenta.
O conteúdo assume que você já tem familiaridade com AWS e pelo menos dois anos de experiência como desenvolvedor. Se você ainda está começando com conceitos de DevOps, recomendo passar primeiro pelo nosso guia sobre DevOps para Devs, onde cobrimos Docker, CI/CD, Kubernetes e AWS em produção.
Por que usar Terraform com AWS
A AWS oferece o CloudFormation como ferramenta nativa de infraestrutura como código. Funciona bem, mas o Terraform traz vantagens que fazem diferença no dia a dia:
Linguagem declarativa com HCL: o HashiCorp Configuration Language é mais legível que JSON ou YAML extenso do CloudFormation. Você descreve o estado desejado e o Terraform calcula o que precisa mudar.
Multi-cloud por natureza: se hoje você está 100% na AWS mas amanhã precisa de um cluster no GCP ou um CDN na Cloudflare, o Terraform unifica tudo com a mesma linguagem e workflow.
State management robusto: o arquivo de estado permite que o Terraform saiba exatamente o que existe na sua infraestrutura, calcule diffs precisos e detecte drift (quando alguém muda algo manualmente).
Ecossistema de módulos: o Terraform Registry tem milhares de módulos prontos para padrões comuns na AWS, como VPCs, clusters ECS e distribuições CloudFront.
Casos de uso onde essa combinação brilha: provisionamento de ambientes efêmeros para testes de integração, replicação idêntica entre staging e produção, governança de infraestrutura via pull requests e destruição automatizada de recursos para controle de custos.
Setup: Instalação e Configuração Inicial
Instalando o Terraform
No macOS com Homebrew:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
terraform -versionNo Linux (Ubuntu/Debian):
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
terraform -versionConfigurando credenciais AWS
A forma recomendada é usar o AWS CLI com profiles nomeados. Nunca coloque access keys diretamente nos arquivos .tf:
aws configure --profile meu-projeto
# AWS Access Key ID: sua-access-key
# AWS Secret Access Key: sua-secret-key
# Default region name: sa-east-1
# Default output format: json
export AWS_PROFILE=meu-projetoPara ambientes de CI/CD, prefira IAM Roles com OIDC (GitHub Actions, GitLab CI) em vez de access keys estáticas. Use roles IAM com menor privilégio: nunca use credenciais de root, crie roles específicas com apenas as permissões que cada serviço precisa. Por exemplo, se o Terraform só vai gerenciar EC2 e S3, a role não precisa de acesso a IAM ou RDS.
Configurando o backend remoto com S3
O state do Terraform precisa ser compartilhado entre o time. A abordagem atual (disponível desde 2024) é usar o S3 com native locking, sem necessidade de uma tabela DynamoDB separada:
# Crie o bucket para o state (faça isso uma única vez)
aws s3api create-bucket \
--bucket meu-projeto-terraform-state \
--region sa-east-1 \
--create-bucket-configuration LocationConstraint=sa-east-1
# Habilite versionamento (essencial para recuperação)
aws s3api put-bucket-versioning \
--bucket meu-projeto-terraform-state \
--versioning-configuration Status=EnabledA abordagem legada utilizava uma tabela DynamoDB para locking. Se você encontrar configurações com dynamodb_table no bloco de backend, saiba que ainda funciona, mas o native locking do S3 simplifica a arquitetura eliminando esse recurso adicional.
Exemplo prático de infraestrutura para uma aplicação web
Vamos provisionar a infraestrutura do exemplo: VPC com subnets públicas, uma instância EC2 preparada com Docker, um banco RDS PostgreSQL e um bucket S3 para assets. O user_data instala e inicia o serviço Docker; a publicação da imagem e a execução da aplicação precisam de uma etapa de deploy própria. Ao concluir o provisionamento, ainda não há uma aplicação web demonstrada em execução.
Estrutura de arquivos
projeto-infra/
├── main.tf
├── variables.tf
├── outputs.tf
├── providers.tf
└── terraform.tfvarsArquivo providers.tf
terraform {
required_version = ">= 1.10.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "meu-projeto-terraform-state"
key = "prod/terraform.tfstate"
region = "sa-east-1"
use_lockfile = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "terraform"
Project = var.project_name
}
}
}O bloqueio nativo do state no S3 exige Terraform 1.10.0 ou superior. Confira também a versão utilizada pelo CI e pelos demais integrantes do time. A identidade que acessa o backend precisa das permissões de leitura e escrita do state e de GetObject, PutObject e DeleteObject para o objeto de bloqueio terminado em .tflock.
Arquivo variables.tf
variable "aws_region" {
description = "Região AWS para deploy dos recursos"
type = string
default = "sa-east-1"
}
variable "environment" {
description = "Nome do ambiente (dev, staging, prod)"
type = string
}
variable "project_name" {
description = "Nome do projeto usado para naming dos recursos"
type = string
default = "minha-app"
}
variable "db_password" {
description = "Senha do banco de dados RDS"
type = string
sensitive = true
}
variable "instance_type" {
description = "Tipo da instância EC2"
type = string
default = "t3.micro"
}Arquivo main.tf
# =====================
# VPC e Networking
# =====================
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc"
}
}
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "${var.aws_region}a"
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-a"
}
}
resource "aws_subnet" "public_b" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "${var.aws_region}b"
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-b"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
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
}
}
resource "aws_route_table_association" "public_a" {
subnet_id = aws_subnet.public_a.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public_b" {
subnet_id = aws_subnet.public_b.id
route_table_id = aws_route_table.public.id
}
# =====================
# Security Groups
# =====================
resource "aws_security_group" "web" {
name_prefix = "${var.project_name}-web-"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_security_group" "db" {
name_prefix = "${var.project_name}-db-"
vpc_id = aws_vpc.main.id
ingress {
description = "PostgreSQL from web servers"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
}
# =====================
# EC2
# =====================
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = aws_subnet.public_a.id
vpc_security_group_ids = [aws_security_group.web.id]
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y docker
systemctl start docker
systemctl enable docker
EOF
tags = {
Name = "${var.project_name}-web"
}
}
# =====================
# RDS PostgreSQL
# =====================
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-db-subnet"
subnet_ids = [aws_subnet.public_a.id, aws_subnet.public_b.id]
}
resource "aws_db_instance" "postgres" {
identifier = "${var.project_name}-db"
engine = "postgres"
engine_version = "16.3"
instance_class = "db.t3.micro"
allocated_storage = 20
storage_encrypted = true
db_name = "appdb"
username = "appadmin"
password = var.db_password
skip_final_snapshot = var.environment != "prod"
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
}
# =====================
# S3 para Assets
# =====================
resource "aws_s3_bucket" "assets" {
bucket = "${var.project_name}-assets-${var.environment}"
}
resource "aws_s3_bucket_public_access_block" "assets" {
bucket = aws_s3_bucket.assets.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}Arquivo outputs.tf
output "ec2_public_ip" {
description = "IP público da instância web"
value = aws_instance.web.public_ip
}
output "rds_endpoint" {
description = "Endpoint de conexão do RDS"
value = aws_db_instance.postgres.endpoint
}
output "s3_bucket_name" {
description = "Nome do bucket S3 de assets"
value = aws_s3_bucket.assets.id
}Ciclo completo de comandos
# Inicializar o projeto (baixa providers, configura backend)
terraform init
# Validar a sintaxe dos arquivos
terraform validate
# Formatar o código automaticamente
terraform fmt -recursive
# Visualizar o plano de execução
terraform plan -var-file="terraform.tfvars"
# Aplicar as mudanças (com aprovação manual)
terraform apply -var-file="terraform.tfvars"
# Verificar o estado atual dos recursos
terraform state list
# Inspecionar um recurso específico
terraform state show aws_instance.web
# Destruir TODA a infraestrutura (cuidado em produção)
terraform destroy -var-file="terraform.tfvars"O arquivo terraform.tfvars para esse projeto ficaria assim:
environment = "staging"
project_name = "minha-app"
db_password = "use-secrets-manager-em-prod"
instance_type = "t3.micro"Em produção, nunca versione o terraform.tfvars com senhas. Use variáveis de ambiente (TF_VAR_db_password) ou integre com o AWS Secrets Manager.
Boas Práticas para Terraform na AWS
Organize com módulos reutilizáveis
Quando seu main.tf passa de 200 linhas, é hora de extrair módulos. Crie um diretório modules/ com componentes isolados como networking, compute e database. Cada módulo recebe variáveis, gerencia seus próprios recursos e expõe outputs.
Use workspaces ou diretórios por ambiente
A forma mais segura de separar ambientes é com diretórios distintos (environments/dev/, environments/prod/), cada um com seu próprio state. Workspaces do Terraform funcionam, mas podem causar confusão quando o time é grande, pois um terraform apply acidental no workspace errado causa estragos.
Trave versões de providers
A restrição ~> 5.0 aceita versões a partir de 5.0.0 e menores que 6.0.0, incluindo atualizações minor da série 5. Para limitar as atualizações à série de patches 5.0.x, use ~> 5.0.0, equivalente a >= 5.0.0 e < 5.1.0. Escolha a faixa conscientemente e versione .terraform.lock.hcl para registrar a versão do provider selecionada. Atualizações desse arquivo devem ser revisadas e validadas.
Implemente políticas com Sentinel ou OPA
Para times maiores, valide automaticamente que ninguém está criando Security Groups abertos ao mundo ou instâncias sem criptografia. Ferramentas como OPA (Open Policy Agent) com o conftest rodam no CI antes do apply.
Tagueie tudo
O bloco default_tags no provider AWS (como fizemos no exemplo) garante que todo recurso receba tags de ambiente, projeto e ferramenta de gerenciamento. Isso é essencial para controle de custos e auditoria.
Erros Comuns e Soluções
Error: No valid credential sources found
Diagnóstico: o Terraform não encontrou credenciais AWS configuradas.
Solução: verifique se o AWS_PROFILE está exportado ou se as variáveis AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY existem no ambiente:
export AWS_PROFILE=meu-projeto
# ou
aws sts get-caller-identity # testa se as credenciais estão ativasError: Failed to get existing workspaces: S3 bucket does not exist
Diagnóstico: o bucket de state configurado no backend não existe ou o nome está errado.
Solução: crie o bucket manualmente antes de rodar terraform init. Buckets de state não devem ser gerenciados pelo próprio Terraform que os usa:
aws s3api create-bucket --bucket nome-correto-do-bucket --region sa-east-1 \
--create-bucket-configuration LocationConstraint=sa-east-1Error: resource already exists
Diagnóstico: você está tentando criar um recurso que já existe na AWS mas não está no state do Terraform. Isso acontece quando alguém cria recursos manualmente pelo console.
Solução: importe o recurso existente para o state:
terraform import aws_instance.web i-0abc123def456789Error: cycle detected in dependency graph
Diagnóstico: dois ou mais recursos dependem um do outro de forma circular. Exemplo comum: um Security Group que referencia outro, que referencia o primeiro.
Solução: quebre o ciclo usando aws_security_group_rule como recurso separado em vez de blocos ingress inline no Security Group. Isso permite que o Terraform resolva as dependências em etapas.
Error: InvalidParameterValue: CIDR block conflicts with existing subnet
Diagnóstico: o bloco CIDR da subnet que você está tentando criar sobrepõe um bloco já existente na VPC.
Solução: verifique os CIDRs existentes e ajuste o endereçamento. Use uma calculadora de subnets para garantir que não há sobreposição:
aws ec2 describe-subnets --filters "Name=vpc-id,Values=vpc-xxx" \
--query "Subnets[].CidrBlock"Error: Provider produced inconsistent result after apply
Diagnóstico: o provider retornou um estado diferente do esperado após aplicar uma mudança. Normalmente ocorre com recursos que têm valores computados pela AWS (como nomes de domínio de RDS).
Solução: rode terraform refresh para sincronizar o state com a realidade da AWS, e então rode terraform plan novamente:
terraform refresh -var-file="terraform.tfvars"
terraform plan -var-file="terraform.tfvars"Erro silencioso: drift não detectado
Diagnóstico: alguém alterou um recurso pelo console e o Terraform não sabe. O próximo apply pode reverter a mudança ou conflitar.
Solução: rode terraform plan regularmente (no CI, por exemplo) para detectar drift. Considere ferramentas como driftctl para varreduras completas da conta AWS.
Conclusão
O Terraform na AWS transforma a maneira como você gerencia infraestrutura: de cliques manuais no console para código revisável, testável e reproduzível. O investimento inicial em aprender HCL e estruturar seus módulos se paga rapidamente quando você precisa replicar um ambiente inteiro em minutos ou auditar quem mudou o quê e quando.
Os pontos essenciais para levar deste guia: configure o backend remoto com S3 desde o início, aplique o princípio de menor privilégio nas credenciais que o Terraform usa, versione seus providers, nunca armazene senhas em arquivos de configuração e rode terraform plan no CI antes de qualquer merge.
Para aprofundar seus conhecimentos em containers e pipelines de deploy que complementam o Terraform, confira o guia sobre DevOps para Devs: Docker, CI/CD, Kubernetes e AWS em Produção, onde cobrimos como essas ferramentas trabalham juntas em um fluxo completo. Outro tema que vale explorar em um guia futuro é a integração do Terraform com GitHub Actions para automação completa do ciclo de infraestrutura.
Comece pequeno: provisione um bucket S3 e uma instância EC2 com Terraform. Depois evolua para módulos, múltiplos ambientes e políticas automatizadas. A curva de aprendizado é real, mas a confiança que você ganha ao fazer terraform apply sabendo exatamente o que vai acontecer não tem preço.
Guias Relacionados
Explore mais guias técnicos
Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.
Ver todos os guias