Saltar para o conteúdo
Terraform com AWS: Infraestrutura como Código na Prática
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 state

Evite 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_REQUEST

Mó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 tfplan

Comandos 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 refresh

A 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.