AWS
Terraform com AWS: Infraestrutura como Código na Prática
5 de setembro de 2024·Paulo de Paula
Terraform transforma infra em código versionável, revisável e reproduzível. Mas o poder real aparece quando você vai além do terraform apply básico e adota as práticas que times sérios usam.
Estrutura de projeto que escala
infra/
├── environments/
│ ├── staging/
│ │ ├── main.tf # usa módulos
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── production/
│ ├── main.tf
│ └── terraform.tfvars
├── modules/
│ ├── vpc/
│ ├── ecs-service/
│ └── rds-postgres/
└── shared/
└── backend.tf # S3 + DynamoDB para stateEvite um único main.tf gigante. Módulos por recurso lógico permitem reusar e testar separadamente.
State remoto com S3 + DynamoDB
# shared/backend.tf
terraform {
backend "s3" {
bucket = "minha-empresa-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
encrypt = true
# Prevent concurrent applies
dynamodb_table = "terraform-state-lock"
}
}# Cria os recursos de estado uma única vez (bootstrap)
aws s3api create-bucket \
--bucket minha-empresa-terraform-state \
--region us-east-1
aws dynamodb create-table \
--table-name terraform-state-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUESTMódulo ECS Service reutilizável
# modules/ecs-service/main.tf
variable "name" {}
variable "cluster_arn" {}
variable "image" {}
variable "cpu" { default = 256 }
variable "memory" { default = 512 }
variable "port" { default = 3000 }
variable "desired_count" { default = 2 }
variable "environment" { default = [] }
variable "secrets" { default = [] }
resource "aws_ecs_task_definition" "this" {
family = var.name
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = var.cpu
memory = var.memory
execution_role_arn = aws_iam_role.exec.arn
task_role_arn = aws_iam_role.task.arn
container_definitions = jsonencode([{
name = var.name
image = var.image
essential = true
environment = var.environment
secrets = var.secrets
portMappings = [{ containerPort = var.port }]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/${var.name}"
"awslogs-region" = data.aws_region.current.name
"awslogs-stream-prefix" = "ecs"
}
}
}])
}
resource "aws_ecs_service" "this" {
name = var.name
cluster = var.cluster_arn
task_definition = aws_ecs_task_definition.this.arn
desired_count = var.desired_count
launch_type = "FARGATE"
network_configuration {
subnets = data.aws_subnets.private.ids
security_groups = [aws_security_group.service.id]
}
load_balancer {
target_group_arn = aws_lb_target_group.this.arn
container_name = var.name
container_port = var.port
}
deployment_circuit_breaker {
enable = true
rollback = true
}
}Usando o módulo em produção:
# environments/production/main.tf
module "api" {
source = "../../modules/ecs-service"
name = "minha-api"
cluster_arn = module.cluster.arn
image = "${module.ecr.url}:${var.image_tag}"
cpu = 512
memory = 1024
desired_count = 3
environment = [
{ name = "NODE_ENV", value = "production" },
{ name = "LOG_LEVEL", value = "info" },
]
secrets = [
{ name = "DATABASE_URL", valueFrom = aws_ssm_parameter.db_url.arn },
{ name = "JWT_SECRET", valueFrom = aws_ssm_parameter.jwt.arn },
]
}Workspaces para múltiplos ambientes
# Cria workspace por ambiente
terraform workspace new staging
terraform workspace new production
# Aplica no ambiente correto
terraform workspace select staging
terraform apply -var-file="staging.tfvars"
terraform workspace select production
terraform apply -var-file="production.tfvars"# Usa o workspace no código
locals {
is_production = terraform.workspace == "production"
instance_count = local.is_production ? 3 : 1
instance_type = local.is_production ? "t3.medium" : "t3.small"
}Pipeline de CI/CD com GitHub Actions
# .github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [main]
paths: ['infra/**']
pull_request:
paths: ['infra/**']
jobs:
plan:
runs-on: ubuntu-latest
permissions:
id-token: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE }}
aws-region: us-east-1
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.9.0"
- run: terraform init
working-directory: infra/environments/production
- run: terraform plan -out=tfplan
working-directory: infra/environments/production
# Posta o plano no PR como comentário
- uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
script: |
const output = `${{ steps.plan.outputs.stdout }}`
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `\`\`\`\n${output}\n\`\`\``
})
apply:
needs: plan
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production # requer aprovação manual
steps:
- run: terraform apply tfplanComandos do dia a dia
# Formata todo o código
terraform fmt -recursive
# Valida sem acessar a AWS
terraform validate
# Mostra o estado atual
terraform show
# Remove um recurso do state sem destruí-lo na AWS
terraform state rm aws_instance.old_server
# Importa recurso criado manualmente
terraform import aws_s3_bucket.logs meu-bucket-de-logs
# Atualiza state sem mudar infra
terraform refreshA maior armadilha do Terraform em equipes: não revisar o terraform plan antes de aplicar. Nunca faça terraform apply em produção sem ler o plano completo — uma linha de diff pode apagar um banco de dados.