Ir para o conteúdo
Ferramenta para Plataforma2373 palavras

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:

SHELL
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
terraform -version

No Linux (Ubuntu/Debian):

SHELL
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 -version

Configurando credenciais AWS

A forma recomendada é usar o AWS CLI com profiles nomeados. Nunca coloque access keys diretamente nos arquivos .tf:

SHELL
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-projeto

Para 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:

SHELL
# 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=Enabled

A 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

SHELL
projeto-infra/
├── main.tf
├── variables.tf
├── outputs.tf
├── providers.tf
└── terraform.tfvars

Arquivo providers.tf

HCL
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

HCL
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

HCL
# =====================
# 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

HCL
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

SHELL
# 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:

HCL
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:

SHELL
export AWS_PROFILE=meu-projeto
# ou
aws sts get-caller-identity  # testa se as credenciais estão ativas

Error: 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:

SHELL
aws s3api create-bucket --bucket nome-correto-do-bucket --region sa-east-1 \
  --create-bucket-configuration LocationConstraint=sa-east-1

Error: 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:

SHELL
terraform import aws_instance.web i-0abc123def456789

Error: 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:

SHELL
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:

SHELL
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