DevOps

Artigo 26 — Seus Primeiros Recursos na AWS com Terraform Já leu

12 min de leitura

Artigo 26 — Seus Primeiros Recursos na AWS com Terraform
Artigo 26 — Seus Primeiros Recursos na AWS com Terraform Colocando Infraestrutura Real no Ar O artigo anterior introduziu o Terraform com um exemplo que já

Artigo 26 — Seus Primeiros Recursos na AWS com Terraform

Módulo 5 · Infraestrutura como Código Prof. Ricardo Matos — Dominando DevOps & Cloud em 1 Ano


Colocando Infraestrutura Real no Ar

O artigo anterior introduziu o Terraform com um exemplo que já incluía alguns recursos AWS. Este artigo expande esse escopo de forma sistemática: serão criados os recursos fundamentais que formam a base de qualquer infraestrutura na AWS — rede, computação, armazenamento e banco de dados — todos gerenciados pelo Terraform, todos versionáveis e reproduzíveis.

O objetivo ao final deste artigo é ter uma infraestrutura funcional completa provisionada inteiramente por código, compreendendo uma VPC com subnets públicas e privadas, instâncias EC2, um bucket S3 e um banco de dados RDS — os mesmos componentes que sustentam a maioria das aplicações web em produção.


Configurando as Credenciais AWS

Antes de qualquer coisa, o Terraform precisa de credenciais para interagir com a AWS. A forma recomendada é via variáveis de ambiente — nunca hardcoded nos arquivos de configuração:

# Configura credenciais via AWS CLI (forma mais conveniente)
aws configure
# AWS Access Key ID: AKIA...
# AWS Secret Access Key: ...
# Default region name: us-east-1
# Default output format: json

# Ou via variáveis de ambiente diretamente
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_DEFAULT_REGION="us-east-1"

# Verifica que as credenciais estão funcionando
aws sts get-caller-identity

Para ambientes de produção e pipelines de CI/CD, a abordagem mais segura é usar IAM Roles com OIDC — sem chaves estáticas — conforme abordado no artigo sobre secrets. Para desenvolvimento local, as credenciais via aws configure são suficientes.


Construindo uma VPC Completa

A Virtual Private Cloud é o componente de rede fundamental da AWS. Toda infraestrutura séria começa com uma VPC bem configurada — não com a VPC padrão que a AWS cria automaticamente.

# networking.tf

# VPC principal
resource "aws_vpc" "principal" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.project_name}-${var.environment}-vpc"
  }
}

# Internet Gateway — permite tráfego de entrada e saída da internet
resource "aws_internet_gateway" "principal" {
  vpc_id = aws_vpc.principal.id

  tags = {
    Name = "${var.project_name}-${var.environment}-igw"
  }
}

# Subnets públicas — para load balancers e bastion hosts
resource "aws_subnet" "publica" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.principal.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}-subnet-publica-${count.index + 1}"
    Tier = "public"
  }
}

# Subnets privadas — para aplicação e banco de dados
resource "aws_subnet" "privada" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.principal.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}-subnet-privada-${count.index + 1}"
    Tier = "private"
  }
}

# Elastic IPs para os NAT Gateways
resource "aws_eip" "nat" {
  count  = length(var.availability_zones)
  domain = "vpc"

  tags = {
    Name = "${var.project_name}-${var.environment}-eip-nat-${count.index + 1}"
  }
}

# NAT Gateways — permitem que as subnets privadas acessem a internet
# sem ficarem expostas a conexões de entrada
resource "aws_nat_gateway" "principal" {
  count         = length(var.availability_zones)
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.publica[count.index].id

  tags = {
    Name = "${var.project_name}-${var.environment}-nat-${count.index + 1}"
  }

  depends_on = [aws_internet_gateway.principal]
}

# Tabela de rotas para subnets públicas
resource "aws_route_table" "publica" {
  vpc_id = aws_vpc.principal.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.principal.id
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-rt-publica"
  }
}

# Tabelas de rotas para subnets privadas
resource "aws_route_table" "privada" {
  count  = length(var.availability_zones)
  vpc_id = aws_vpc.principal.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.principal[count.index].id
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-rt-privada-${count.index + 1}"
  }
}

# Associações de tabelas de rotas — subnets públicas
resource "aws_route_table_association" "publica" {
  count          = length(var.availability_zones)
  subnet_id      = aws_subnet.publica[count.index].id
  route_table_id = aws_route_table.publica.id
}

# Associações de tabelas de rotas — subnets privadas
resource "aws_route_table_association" "privada" {
  count          = length(var.availability_zones)
  subnet_id      = aws_subnet.privada[count.index].id
  route_table_id = aws_route_table.privada[count.index].id
}

Grupos de Segurança

# security_groups.tf

# Security group para o load balancer — aceita tráfego da internet
resource "aws_security_group" "load_balancer" {
  name        = "${var.project_name}-${var.environment}-sg-alb"
  description = "Security group do Application Load Balancer"
  vpc_id      = aws_vpc.principal.id

  ingress {
    description = "HTTP da internet"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "HTTPS da internet"
    from_port   = 443
    to_port     = 443
    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"]
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-sg-alb"
  }
}

# Security group para as instâncias de aplicação
# Aceita tráfego apenas do load balancer
resource "aws_security_group" "aplicacao" {
  name        = "${var.project_name}-${var.environment}-sg-app"
  description = "Security group das instâncias de aplicação"
  vpc_id      = aws_vpc.principal.id

  ingress {
    description     = "Tráfego do load balancer"
    from_port       = 3000
    to_port         = 3000
    protocol        = "tcp"
    security_groups = [aws_security_group.load_balancer.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-sg-app"
  }
}

# Security group para o banco de dados
# Aceita tráfego apenas das instâncias de aplicação
resource "aws_security_group" "banco_dados" {
  name        = "${var.project_name}-${var.environment}-sg-db"
  description = "Security group do banco de dados RDS"
  vpc_id      = aws_vpc.principal.id

  ingress {
    description     = "PostgreSQL das instâncias de aplicação"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.aplicacao.id]
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-sg-db"
  }
}

Instâncias EC2 com Auto Scaling

Em vez de criar instâncias individuais, a prática recomendada para aplicações web é usar um Auto Scaling Group — um conjunto de instâncias que escala automaticamente com base na demanda:

# compute.tf

# IAM Role para as instâncias EC2
resource "aws_iam_role" "instancia_ec2" {
  name = "${var.project_name}-${var.environment}-ec2-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "ec2.amazonaws.com"
      }
    }]
  })
}

# Política para acesso ao SSM — permite acesso sem SSH exposto
resource "aws_iam_role_policy_attachment" "ssm" {
  role       = aws_iam_role.instancia_ec2.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

# Instance Profile — associa a Role à instância
resource "aws_iam_instance_profile" "aplicacao" {
  name = "${var.project_name}-${var.environment}-instance-profile"
  role = aws_iam_role.instancia_ec2.name
}

# Launch Template — modelo para criação de instâncias
resource "aws_launch_template" "aplicacao" {
  name_prefix   = "${var.project_name}-${var.environment}-"
  image_id      = data.aws_ami.ubuntu.id
  instance_type = var.instance_type

  iam_instance_profile {
    arn = aws_iam_instance_profile.aplicacao.arn
  }

  network_interfaces {
    associate_public_ip_address = false
    security_groups             = [aws_security_group.aplicacao.id]
  }

  block_device_mappings {
    device_name = "/dev/sda1"
    ebs {
      volume_size           = 20
      volume_type           = "gp3"
      encrypted             = true
      delete_on_termination = true
    }
  }

  user_data = base64encode(<<-EOF
    #!/bin/bash
    apt-get update -y
    apt-get install -y docker.io docker-compose-plugin awscli

    systemctl enable docker
    systemctl start docker
    usermod -aG docker ubuntu

    # Baixa e inicia a aplicação
    aws ecr get-login-password --region ${var.aws_region} | \
      docker login --username AWS --password-stdin \
      ${data.aws_caller_identity.atual.account_id}.dkr.ecr.${var.aws_region}.amazonaws.com

    docker run -d \
      --name minha-api \
      --restart unless-stopped \
      -p 3000:3000 \
      ${var.app_image}
  EOF
  )

  tag_specifications {
    resource_type = "instance"
    tags = {
      Name = "${var.project_name}-${var.environment}-app"
    }
  }
}

# Auto Scaling Group
resource "aws_autoscaling_group" "aplicacao" {
  name                = "${var.project_name}-${var.environment}-asg"
  vpc_zone_identifier = aws_subnet.privada[*].id
  min_size            = var.asg_min_size
  max_size            = var.asg_max_size
  desired_capacity    = var.asg_desired_capacity

  launch_template {
    id      = aws_launch_template.aplicacao.id
    version = "$Latest"
  }

  health_check_type         = "ELB"
  health_check_grace_period = 60

  tag {
    key                 = "Name"
    value               = "${var.project_name}-${var.environment}-app"
    propagate_at_launch = true
  }

  lifecycle {
    create_before_destroy = true
  }
}

Bucket S3 com Configurações de Segurança

# storage.tf

resource "aws_s3_bucket" "assets" {
  bucket = "${var.project_name}-${var.environment}-assets-${data.aws_caller_identity.atual.account_id}"

  tags = {
    Name = "${var.project_name}-${var.environment}-assets"
  }
}

# Bloqueia todo acesso público — configuração de segurança fundamental
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
}

# Habilita versionamento
resource "aws_s3_bucket_versioning" "assets" {
  bucket = aws_s3_bucket.assets.id

  versioning_configuration {
    status = "Enabled"
  }
}

# Habilita criptografia server-side
resource "aws_s3_bucket_server_side_encryption_configuration" "assets" {
  bucket = aws_s3_bucket.assets.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# Política de ciclo de vida — move objetos antigos para storage mais barato
resource "aws_s3_bucket_lifecycle_configuration" "assets" {
  bucket = aws_s3_bucket.assets.id

  rule {
    id     = "mover-para-ia"
    status = "Enabled"

    transition {
      days          = 30
      storage_class = "STANDARD_IA"
    }

    transition {
      days          = 90
      storage_class = "GLACIER"
    }

    expiration {
      days = 365
    }
  }
}

Banco de Dados RDS

# database.tf

# Subnet group para o RDS — define em quais subnets o banco pode ficar
resource "aws_db_subnet_group" "principal" {
  name       = "${var.project_name}-${var.environment}-db-subnet-group"
  subnet_ids = aws_subnet.privada[*].id

  tags = {
    Name = "${var.project_name}-${var.environment}-db-subnet-group"
  }
}

# Parâmetros customizados do PostgreSQL
resource "aws_db_parameter_group" "postgres" {
  name   = "${var.project_name}-${var.environment}-pg16"
  family = "postgres16"

  parameter {
    name  = "log_connections"
    value = "1"
  }

  parameter {
    name  = "log_disconnections"
    value = "1"
  }

  parameter {
    name  = "log_min_duration_statement"
    value = "1000"  # loga queries acima de 1 segundo
  }
}

# Instância RDS PostgreSQL
resource "aws_db_instance" "principal" {
  identifier = "${var.project_name}-${var.environment}-db"

  engine         = "postgres"
  engine_version = "16.1"
  instance_class = var.db_instance_class

  allocated_storage     = 20
  max_allocated_storage = 100  # auto scaling de storage até 100GB
  storage_type          = "gp3"
  storage_encrypted     = true

  db_name  = var.db_name
  username = var.db_username
  password = var.db_password

  db_subnet_group_name   = aws_db_subnet_group.principal.name
  parameter_group_name   = aws_db_parameter_group.postgres.name
  vpc_security_group_ids = [aws_security_group.banco_dados.id]

  multi_az               = var.environment == "production"
  publicly_accessible    = false
  deletion_protection    = var.environment == "production"
  skip_final_snapshot    = var.environment != "production"

  final_snapshot_identifier = var.environment == "production" ? \
    "${var.project_name}-${var.environment}-final-snapshot" : null

  backup_retention_period = var.environment == "production" ? 7 : 1
  backup_window           = "03:00-04:00"
  maintenance_window      = "Mon:04:00-Mon:05:00"

  performance_insights_enabled = true

  tags = {
    Name = "${var.project_name}-${var.environment}-db"
  }
}

Outputs Completos

# outputs.tf

output "vpc_id" {
  description = "ID da VPC criada"
  value       = aws_vpc.principal.id
}

output "subnets_publicas" {
  description = "IDs das subnets públicas"
  value       = aws_subnet.publica[*].id
}

output "subnets_privadas" {
  description = "IDs das subnets privadas"
  value       = aws_subnet.privada[*].id
}

output "endpoint_rds" {
  description = "Endpoint de conexão do banco de dados"
  value       = aws_db_instance.principal.endpoint
  sensitive   = true
}

output "bucket_assets" {
  description = "Nome do bucket S3 de assets"
  value       = aws_s3_bucket.assets.id
}

output "database_url" {
  description = "URL de conexão com o banco formatada"
  value = "postgresql://${var.db_username}:${var.db_password}@${aws_db_instance.principal.endpoint}/${var.db_name}"
  sensitive = true
}

O Que Vem a Seguir

Com a infraestrutura básica na AWS funcionando via Terraform, o próximo artigo aprofunda o tema de módulos — como organizar configurações reutilizáveis que podem ser compartilhadas entre projetos e ambientes. Módulos são o mecanismo que transforma código Terraform de scripts pontuais em uma biblioteca de infraestrutura corporativa.


Referências para Aprofundamento

Documentação e referências - Terraform AWS Provider — registry.terraform.io — Referência completa de todos os recursos AWS disponíveis no provider Terraform, com exemplos detalhados. - AWS VPC Documentation — Documentação oficial da AWS sobre VPC, cobrindo conceitos de subnets, route tables e security groups.

Tutoriais práticos - HashiCorp Learn — Build Infrastructure — Tutorial oficial da HashiCorp para provisionar infraestrutura na AWS com Terraform, com ambiente de laboratório incluso. - Terraform AWS Modules — GitHub — Coleção de módulos Terraform oficialmente mantidos para os serviços AWS mais comuns, incluindo VPC, EC2 e RDS. Excelente referência para boas práticas de organização.

Segurança - Checkov — Bridgecrew — Ferramenta de análise estática para código Terraform que verifica boas práticas de segurança. Pode ser integrada ao pipeline de CI para bloquear configurações inseguras antes do apply.


Artigo 26 de 52 · Módulo 5 — Infraestrutura como Código Prof. Ricardo Matos · Série Dominando DevOps & Cloud em 1 Ano


you asked

Continue


claude response

Comentários

Mais em DevOps

Bitbucket e o Ecossistema Atlassian
Bitbucket e o Ecossistema Atlassian

Há uma situação muito específica que justifica o uso do Bitbucket: a organiza...

Trabalhando em Equipe com Git Flow
Trabalhando em Equipe com Git Flow

O Git oferece branches, mas n&atilde;o diz como us&aacute;-las. Em uma equipe...

RDS, ElastiCache e Estratégias de Dados na AWS
RDS, ElastiCache e Estratégias de Dados na AWS

Em qualquer sistema de produção, a camada de dados é simultaneamente a mais c...