Route53, CloudFront e ACM: Rede e Entrega de Conteúdo

[306] Route53, CloudFront e ACM: Rede e Entrega de Conteúdo

A camada de entrega que fica entre o usuário e a aplicação: hosted zones e alias records no Route53, roteamento por latência e failover com health checks, certificados do ACM e a exigência de us-east-1, cache policies e headers de segurança no CloudFront, e o WAF com rate limiting.
DevOps

24 min de leitura

Uma aplicação pode estar perfeitamente construída — código limpo, banco de dados otimizado, infraestrutura resiliente — e ainda assim oferecer uma experiência ruim ao usuário se a camada de entrega não estiver bem configurada. Latência alta por distância geográfica, certificados SSL expirados, DNS mal configurado, ausência de cache para conteúdo estático — esses problemas são invisíveis para quem desenvolve localmente e devastadores para quem usa o sistema em produção.

Os três serviços abordados neste artigo formam a camada de entrega da AWS: o Route53 gerencia o DNS, o CloudFront distribui o conteúdo globalmente com baixa latência, e o ACM provê e renova certificados SSL/TLS automaticamente. Juntos, eles fazem com que uma requisição de São Paulo para uma aplicação hospedada em us-east-1 seja respondida com baixa latência, com HTTPS e com os headers de segurança corretos.

Route53: DNS como Infraestrutura

O Route53 é o serviço de DNS da AWS — altamente disponível, distribuído globalmente e integrado nativamente com os demais serviços AWS. A diferença fundamental entre o Route53 e um provedor de DNS tradicional é a capacidade de usar alias records — registros que apontam diretamente para recursos AWS como load balancers, distribuições CloudFront e buckets S3, resolvendo automaticamente para os IPs corretos sem custo de query.

Conceitos Fundamentais

Hosted Zone — o container que agrupa todos os registros DNS de um domínio. Uma hosted zone pública é acessível pela internet. Uma hosted zone privada é acessível apenas dentro de uma VPC — útil para nomes de serviços internos.

Record Types — os tipos de registro mais relevantes na AWS:

A — mapeia um nome para um endereço IPv4. Usado para apontar diretamente para IPs.

AAAA — mapeia um nome para um endereço IPv6.

CNAME — mapeia um nome para outro nome (canonical name). Não pode ser usado na raiz do domínio — empresa.com não pode ter um CNAME, mas www.empresa.com pode.

ALIAS — extensão proprietária do Route53 que se comporta como um CNAME mas pode ser usado na raiz do domínio e não gera custo de query quando aponta para recursos AWS.

MX — registros de email, apontando para servidores de correio.

TXT — texto livre, usado para verificação de domínio (Google Search Console, SPF, DKIM).

Configurando DNS com Terraform

# dns.tf

# Hosted Zone pública para o domínio principal
resource "aws_route53_zone" "principal" {
  name    = var.domain_name
  comment = "Hosted zone principal — ${var.project_name}"

  tags = local.tags_comuns
}

# Registro raiz apontando para o CloudFront
resource "aws_route53_record" "raiz" {
  zone_id = aws_route53_zone.principal.zone_id
  name    = var.domain_name
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.principal.domain_name
    zone_id                = aws_cloudfront_distribution.principal.hosted_zone_id
    evaluate_target_health = false
  }
}

# Registro www com redirect para raiz
resource "aws_route53_record" "www" {
  zone_id = aws_route53_zone.principal.zone_id
  name    = "www.${var.domain_name}"
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.principal.domain_name
    zone_id                = aws_cloudfront_distribution.principal.hosted_zone_id
    evaluate_target_health = false
  }
}

# Registro de API apontando para o ALB
resource "aws_route53_record" "api" {
  zone_id = aws_route53_zone.principal.zone_id
  name    = "api.${var.domain_name}"
  type    = "A"

  alias {
    name                   = aws_lb.aplicacao.dns_name
    zone_id                = aws_lb.aplicacao.zone_id
    evaluate_target_health = true
  }
}

# Registro para ambiente de staging
resource "aws_route53_record" "staging" {
  zone_id = aws_route53_zone.principal.zone_id
  name    = "staging.${var.domain_name}"
  type    = "A"

  alias {
    name                   = aws_lb.staging.dns_name
    zone_id                = aws_lb.staging.zone_id
    evaluate_target_health = true
  }
}

# Hosted Zone privada para serviços internos
resource "aws_route53_zone" "interna" {
  name = "interno.${var.domain_name}"

  vpc {
    vpc_id = module.vpc.vpc_id
  }

  tags = local.tags_comuns
}

# Registro interno para o banco de dados
# Permite trocar o endpoint real sem alterar a configuração das aplicações
resource "aws_route53_record" "banco_dados" {
  zone_id = aws_route53_zone.interna.zone_id
  name    = "db.interno.${var.domain_name}"
  type    = "CNAME"
  ttl     = 60

  records = [aws_db_instance.principal.address]
}

# Registro interno para o Redis
resource "aws_route53_record" "redis" {
  zone_id = aws_route53_zone.interna.zone_id
  name    = "cache.interno.${var.domain_name}"
  type    = "CNAME"
  ttl     = 60

  records = [aws_elasticache_replication_group.redis.primary_endpoint_address]
}

Roteamento Avançado do Route53

O Route53 oferece políticas de roteamento que vão além do DNS simples. Para arquiteturas de alta disponibilidade e distribuição geográfica:

# Roteamento por latência — direciona para a região com menor latência
resource "aws_route53_record" "api_latencia_us" {
  zone_id        = aws_route53_zone.principal.zone_id
  name           = "api.${var.domain_name}"
  type           = "A"
  set_identifier = "us-east-1"

  latency_routing_policy {
    region = "us-east-1"
  }

  alias {
    name                   = aws_lb.api_us_east.dns_name
    zone_id                = aws_lb.api_us_east.zone_id
    evaluate_target_health = true
  }
}

resource "aws_route53_record" "api_latencia_eu" {
  zone_id        = aws_route53_zone.principal.zone_id
  name           = "api.${var.domain_name}"
  type           = "A"
  set_identifier = "eu-west-1"

  latency_routing_policy {
    region = "eu-west-1"
  }

  alias {
    name                   = aws_lb.api_eu_west.dns_name
    zone_id                = aws_lb.api_eu_west.zone_id
    evaluate_target_health = true
  }
}

# Health Check para failover automático
resource "aws_route53_health_check" "api_principal" {
  fqdn              = "api.${var.domain_name}"
  port              = 443
  type              = "HTTPS"
  resource_path     = "/health"
  failure_threshold = 3
  request_interval  = 30

  tags = merge(local.tags_comuns, {
    Name = "api-health-check"
  })
}

# Registro primário com health check
resource "aws_route53_record" "api_primario" {
  zone_id        = aws_route53_zone.principal.zone_id
  name           = "api.${var.domain_name}"
  type           = "A"
  set_identifier = "primario"

  failover_routing_policy {
    type = "PRIMARY"
  }

  health_check_id = aws_route53_health_check.api_principal.id

  alias {
    name                   = aws_lb.aplicacao.dns_name
    zone_id                = aws_lb.aplicacao.zone_id
    evaluate_target_health = true
  }
}

# Registro secundário — ativa automaticamente se o primário falha
resource "aws_route53_record" "api_secundario" {
  zone_id        = aws_route53_zone.principal.zone_id
  name           = "api.${var.domain_name}"
  type           = "A"
  set_identifier = "secundario"

  failover_routing_policy {
    type = "SECONDARY"
  }

  alias {
    name                   = aws_lb.dr_region.dns_name
    zone_id                = aws_lb.dr_region.zone_id
    evaluate_target_health = true
  }
}

ACM: Certificados SSL/TLS Gerenciados

O AWS Certificate Manager provê certificados SSL/TLS gratuitos para uso com serviços AWS. Os certificados são renovados automaticamente — sem o processo manual de renovação anual que historicamente causava incidentes por certificados expirados.

# acm.tf

# Certificado para o domínio principal e subdomínios
resource "aws_acm_certificate" "principal" {
  domain_name       = var.domain_name
  validation_method = "DNS"

  subject_alternative_names = [
    "*.${var.domain_name}",           # Todos os subdomínios
    "api.${var.domain_name}",
    "staging.${var.domain_name}",
  ]

  lifecycle {
    create_before_destroy = true
  }

  tags = local.tags_comuns
}

# Registros DNS para validação do certificado
resource "aws_route53_record" "acm_validacao" {
  for_each = {
    for dvo in aws_acm_certificate.principal.domain_validation_options :
    dvo.domain_name => {
      name   = dvo.resource_record_name
      type   = dvo.resource_record_type
      record = dvo.resource_record_value
    }
  }

  zone_id         = aws_route53_zone.principal.zone_id
  name            = each.value.name
  type            = each.value.type
  ttl             = 60
  records         = [each.value.record]
  allow_overwrite = true
}

# Aguarda a validação ser concluída
resource "aws_acm_certificate_validation" "principal" {
  certificate_arn         = aws_acm_certificate.principal.arn
  validation_record_fqdns = [for record in aws_route53_record.acm_validacao : record.fqdn]
}

# Certificado em us-east-1 — obrigatório para CloudFront
# CloudFront aceita apenas certificados criados em us-east-1
resource "aws_acm_certificate" "cloudfront" {
  provider          = aws.us_east_1
  domain_name       = var.domain_name
  validation_method = "DNS"

  subject_alternative_names = [
    "*.${var.domain_name}",
  ]

  lifecycle {
    create_before_destroy = true
  }

  tags = local.tags_comuns
}

resource "aws_acm_certificate_validation" "cloudfront" {
  provider                = aws.us_east_1
  certificate_arn         = aws_acm_certificate.cloudfront.arn
  validation_record_fqdns = [for record in aws_route53_record.acm_validacao : record.fqdn]
}

CloudFront: CDN Global com Edge Locations

O CloudFront é a CDN da AWS com mais de 600 pontos de presença (edge locations) distribuídos globalmente. Quando um usuário em São Paulo acessa um conteúdo servido pelo CloudFront, a requisição vai para o edge mais próximo — não para us-east-1 diretamente. O edge verifica se tem o conteúdo em cache; se sim, responde localmente; se não, busca na origem e armazena para requisições futuras.

Para aplicações web, o CloudFront serve múltiplos propósitos além do cache: terminação SSL, proteção contra DDoS via AWS Shield Standard (gratuito), georestrição de conteúdo, redirects HTTPS forçados e adição de headers de segurança.

# cloudfront.tf

# Origin Access Control — acesso seguro ao S3 pelo CloudFront
resource "aws_cloudfront_origin_access_control" "s3" {
  name                              = "${var.project_name}-${var.environment}-s3-oac"
  description                       = "OAC para acesso ao S3"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

# Cache Policy para assets estáticos — cache longo
resource "aws_cloudfront_cache_policy" "assets_estaticos" {
  name        = "${var.project_name}-assets-estaticos"
  comment     = "Cache de longa duração para assets com hash no nome"
  default_ttl = 86400     # 1 dia
  max_ttl     = 31536000  # 1 ano
  min_ttl     = 0

  parameters_in_cache_key_and_forwarded_to_origin {
    cookies_config {
      cookie_behavior = "none"
    }
    headers_config {
      header_behavior = "none"
    }
    query_strings_config {
      query_string_behavior = "none"
    }
    enable_accept_encoding_brotli = true
    enable_accept_encoding_gzip   = true
  }
}

# Cache Policy para a API — sem cache por padrão
resource "aws_cloudfront_cache_policy" "api_sem_cache" {
  name        = "${var.project_name}-api-sem-cache"
  comment     = "Sem cache para respostas dinâmicas da API"
  default_ttl = 0
  max_ttl     = 0
  min_ttl     = 0

  parameters_in_cache_key_and_forwarded_to_origin {
    cookies_config {
      cookie_behavior = "none"
    }
    headers_config {
      header_behavior = "none"
    }
    query_strings_config {
      query_string_behavior = "none"
    }
  }
}

# Origin Request Policy — headers a encaminhar para a origem
resource "aws_cloudfront_origin_request_policy" "api" {
  name    = "${var.project_name}-api-origin-policy"
  comment = "Encaminha headers relevantes para a API"

  cookies_config {
    cookie_behavior = "none"
  }

  headers_config {
    header_behavior = "whitelist"
    headers {
      items = [
        "Authorization",
        "Content-Type",
        "X-Request-ID",
        "X-Forwarded-For",
        "CloudFront-Viewer-Country",
      ]
    }
  }

  query_strings_config {
    query_string_behavior = "all"
  }
}

# Response Headers Policy — headers de segurança
resource "aws_cloudfront_response_headers_policy" "seguranca" {
  name    = "${var.project_name}-security-headers"
  comment = "Headers de segurança HTTP aplicados em todas as respostas"

  security_headers_config {
    content_security_policy {
      content_security_policy = join("; ", [
        "default-src 'self'",
        "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
        "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
        "font-src 'self' https://fonts.gstatic.com",
        "img-src 'self' data: https:",
        "connect-src 'self' https://api.${var.domain_name}",
        "frame-ancestors 'none'",
      ])
      override = true
    }

    content_type_options {
      override = true
    }

    frame_options {
      frame_option = "DENY"
      override     = true
    }

    referrer_policy {
      referrer_policy = "strict-origin-when-cross-origin"
      override        = true
    }

    strict_transport_security {
      access_control_max_age_sec = 31536000
      include_subdomains         = true
      preload                    = true
      override                   = true
    }

    xss_protection {
      mode_block = true
      protection = true
      override   = true
    }
  }

  custom_headers_config {
    items {
      header   = "Permissions-Policy"
      value    = "camera=(), microphone=(), geolocation=()"
      override = true
    }
  }
}

# Distribuição CloudFront principal
resource "aws_cloudfront_distribution" "principal" {
  enabled             = true
  is_ipv6_enabled     = true
  comment             = "${var.project_name} ${var.environment}"
  default_root_object = "index.html"
  price_class         = "PriceClass_All"
  aliases             = [var.domain_name, "www.${var.domain_name}"]
  web_acl_id          = aws_wafv2_web_acl.cloudfront.arn

  # ── Origem 1: Assets estáticos no S3 ──────────────
  origin {
    origin_id                = "s3-assets"
    domain_name              = aws_s3_bucket.frontend.bucket_regional_domain_name
    origin_access_control_id = aws_cloudfront_origin_access_control.s3.id
  }

  # ── Origem 2: API no ALB ───────────────────────────
  origin {
    origin_id   = "alb-api"
    domain_name = aws_lb.aplicacao.dns_name

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]

      origin_keepalive_timeout = 60
      origin_read_timeout      = 30
    }

    custom_header {
      name  = "X-CloudFront-Secret"
      value = var.cloudfront_origin_secret
    }
  }

  # ── Comportamento padrão: S3 com cache longo ───────
  default_cache_behavior {
    allowed_methods        = ["GET", "HEAD", "OPTIONS"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "s3-assets"
    viewer_protocol_policy = "redirect-to-https"
    compress               = true

    cache_policy_id            = aws_cloudfront_cache_policy.assets_estaticos.id
    response_headers_policy_id = aws_cloudfront_response_headers_policy.seguranca.id
  }

  # ── Comportamento para a API: sem cache, encaminha tudo ─
  ordered_cache_behavior {
    path_pattern           = "/api/*"
    allowed_methods        = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "alb-api"
    viewer_protocol_policy = "redirect-to-https"
    compress               = true

    cache_policy_id            = aws_cloudfront_cache_policy.api_sem_cache.id
    origin_request_policy_id   = aws_cloudfront_origin_request_policy.api.id
    response_headers_policy_id = aws_cloudfront_response_headers_policy.seguranca.id
  }

  # ── Comportamento para assets com hash: cache máximo ─
  ordered_cache_behavior {
    path_pattern     = "/static/*"
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "s3-assets"

    viewer_protocol_policy = "redirect-to-https"
    compress               = true

    cache_policy_id            = aws_cloudfront_cache_policy.assets_estaticos.id
    response_headers_policy_id = aws_cloudfront_response_headers_policy.seguranca.id
  }

  # SPA: redireciona 404 e 403 do S3 para index.html
  custom_error_response {
    error_code            = 404
    response_code         = 200
    response_page_path    = "/index.html"
    error_caching_min_ttl = 10
  }

  custom_error_response {
    error_code            = 403
    response_code         = 200
    response_page_path    = "/index.html"
    error_caching_min_ttl = 10
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate_validation.cloudfront.certificate_arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }

  logging_config {
    include_cookies = false
    bucket          = "${aws_s3_bucket.cloudfront_logs.id}.s3.amazonaws.com"
    prefix          = "cloudfront/${var.environment}/"
  }

  tags = local.tags_comuns
}

# Bucket S3 para o frontend
resource "aws_s3_bucket" "frontend" {
  bucket = "${var.project_name}-${var.environment}-frontend"
  tags   = local.tags_comuns
}

resource "aws_s3_bucket_public_access_block" "frontend" {
  bucket                  = aws_s3_bucket.frontend.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Política do bucket — permite acesso apenas pelo CloudFront OAC
resource "aws_s3_bucket_policy" "frontend" {
  bucket = aws_s3_bucket.frontend.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid    = "AllowCloudFrontOAC"
      Effect = "Allow"
      Principal = {
        Service = "cloudfront.amazonaws.com"
      }
      Action   = "s3:GetObject"
      Resource = "${aws_s3_bucket.frontend.arn}/*"
      Condition = {
        StringEquals = {
          "AWS:SourceArn" = aws_cloudfront_distribution.principal.arn
        }
      }
    }]
  })
}

Deploy do Frontend no CloudFront

O pipeline de deploy do frontend sincroniza os arquivos com o S3 e invalida o cache do CloudFront para que a nova versão seja servida imediatamente:

# .github/workflows/deploy-frontend.yml
name: Deploy Frontend

on:
  push:
    branches: [main]
    paths: ['frontend/**']

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production

    defaults:
      run:
        working-directory: frontend

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: frontend/package-lock.json

      - name: Instala dependências
        run: npm ci

      - name: Build de produção
        run: npm run build
        env:
          VITE_API_URL: https://api.${{ vars.DOMAIN_NAME }}
          VITE_APP_VERSION: ${{ github.sha }}

      - name: Configura credenciais AWS
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
          aws-region: us-east-1

      - name: Sincroniza assets estáticos com cache longo
        run: |
          # Assets com hash no nome — cache de 1 ano
          aws s3 sync dist/assets/ \
            s3://${{ vars.FRONTEND_BUCKET }}/assets/ \
            --cache-control "public, max-age=31536000, immutable" \
            --delete

      - name: Sincroniza HTML e arquivos de configuração
        run: |
          # HTML e outros arquivos sem hash — sem cache
          aws s3 sync dist/ \
            s3://${{ vars.FRONTEND_BUCKET }}/ \
            --exclude "assets/*" \
            --cache-control "no-cache, no-store, must-revalidate" \
            --delete

      - name: Invalida cache do CloudFront
        run: |
          INVALIDATION_ID=$(aws cloudfront create-invalidation \
            --distribution-id ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }} \
            --paths "/*" \
            --query 'Invalidation.Id' \
            --output text)

          echo "Invalidação criada: $INVALIDATION_ID"

          # Aguarda a invalidação completar
          aws cloudfront wait invalidation-completed \
            --distribution-id ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }} \
            --id $INVALIDATION_ID

          echo "Cache invalidado com sucesso"

      - name: Verifica deploy
        run: |
          sleep 10
          STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
            https://${{ vars.DOMAIN_NAME }})

          if [ "$STATUS" != "200" ]; then
            echo "ERRO: Site retornou status $STATUS"
            exit 1
          fi

          echo "Deploy verificado — status $STATUS"

WAF: Proteção da Camada de Aplicação

O AWS WAF (Web Application Firewall) protege o CloudFront contra ataques comuns na camada de aplicação — SQL injection, XSS, bots maliciosos, requisições de IPs com reputação ruim:

# waf.tf

resource "aws_wafv2_web_acl" "cloudfront" {
  provider = aws.us_east_1  # WAF para CloudFront deve ser em us-east-1
  name     = "${var.project_name}-${var.environment}-waf"
  scope    = "CLOUDFRONT"

  default_action {
    allow {}
  }

  # ── Regras gerenciadas pela AWS ────────────────────

  # Proteção contra ameaças comuns (OWASP Top 10)
  rule {
    name     = "aws-managed-common-rules"
    priority = 10

    override_action { none {} }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "aws-managed-common"
      sampled_requests_enabled   = true
    }
  }

  # Proteção contra bots maliciosos
  rule {
    name     = "aws-managed-bot-control"
    priority = 20

    override_action { none {} }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesBotControlRuleSet"
        vendor_name = "AWS"

        managed_rule_group_configs {
          aws_managed_rules_bot_control_rule_set {
            inspection_level = "COMMON"
          }
        }
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "aws-managed-bot-control"
      sampled_requests_enabled   = true
    }
  }

  # ── Regras customizadas ────────────────────────────

  # Rate limiting global — máximo de 1000 req/5min por IP
  rule {
    name     = "rate-limit-global"
    priority = 30

    action { block {} }

    statement {
      rate_based_statement {
        limit              = 1000
        aggregate_key_type = "IP"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "rate-limit-global"
      sampled_requests_enabled   = true
    }
  }

  # Rate limiting agressivo no endpoint de autenticação
  rule {
    name     = "rate-limit-auth"
    priority = 25

    action { block {} }

    statement {
      rate_based_statement {
        limit              = 50
        aggregate_key_type = "IP"

        scope_down_statement {
          byte_match_statement {
            field_to_match {
              uri_path {}
            }
            positional_constraint = "STARTS_WITH"
            search_string         = "/api/auth"
            text_transformation {
              priority = 0
              type     = "LOWERCASE"
            }
          }
        }
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "rate-limit-auth"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "${var.project_name}-waf"
    sampled_requests_enabled   = true
  }

  tags = local.tags_comuns
}

Outputs e Configurações para a Aplicação

# outputs.tf

output "cloudfront_url" {
  description = "URL da distribuição CloudFront"
  value       = "https://${aws_cloudfront_distribution.principal.domain_name}"
}

output "url_producao" {
  description = "URL pública de produção"
  value       = "https://${var.domain_name}"
}

output "nameservers" {
  description = "Nameservers do Route53 — configurar no registrador de domínio"
  value       = aws_route53_zone.principal.name_servers
}

output "cloudfront_distribution_id" {
  description = "ID da distribuição CloudFront — necessário para invalidação de cache"
  value       = aws_cloudfront_distribution.principal.id
}

output "frontend_bucket" {
  description = "Nome do bucket S3 do frontend"
  value       = aws_s3_bucket.frontend.id
}

Encerrando o Módulo 7

Com este artigo encerra-se o Módulo 7 — AWS em Profundidade. Foram cobertos os serviços fundamentais de computação, banco de dados, cache e rede que formam a espinha dorsal de qualquer aplicação de produção na AWS: EC2 com suas famílias e ciclo de vida, ECS e Lambda para computação gerenciada, RDS e ElastiCache para dados e caching, e Route53, CloudFront e ACM para entrega e segurança.

O Módulo 8 aborda Kubernetes — o orquestrador de containers que, quando a escala e a complexidade justificam, oferece controle e flexibilidade além do que os serviços gerenciados da AWS proporcionam isoladamente.

Referências para Aprofundamento

Documentação oficial AWS

Segurança

Performance

Exercícios

Exercício 1

No dns.tf, o registro raiz é do tipo A mas não contém nenhum IP — tem um bloco alias apontando para o CloudFront. Por que não usar um CNAME, que é o tipo natural para apontar um nome para outro nome? E por que evaluate_target_health é false nesse registro e true no registro api?

Ver resposta

✓ Resposta: Porque a raiz de um domínio não pode ter CNAME. É uma restrição do próprio DNS: o registro no ápice da zona precisa coexistir com os registros SOA e NS obrigatórios, e o CNAME exige exclusividade — nenhum outro registro pode existir com o mesmo nome. Por isso www.empresa.com pode ser CNAME, mas empresa.com não.

O ALIAS é a saída proprietária do Route53: declarado como tipo A, ele é resolvido internamente pela AWS, que devolve os IPs do recurso de destino diretamente na resposta. O cliente enxerga um registro A comum. Vantagem adicional: queries a alias records para recursos AWS não são cobradas.

Quanto ao evaluate_target_health: com true, o Route53 leva em conta a saúde do alvo antes de responder — no registro api isso faz sentido, porque o ALB reporta a saúde dos seus targets e o Route53 pode parar de devolvê-lo. O CloudFront não suporta essa avaliação (é uma rede global de edges, não um endpoint com estado de saúde), então ali o valor é obrigatoriamente false.

Exercício 2

O acm.tf cria dois certificados — principal e cloudfront — cobrindo exatamente os mesmos domínios. Isso é duplicação desnecessária? Justifique olhando para o argumento provider.

Ver resposta

✓ Resposta: Não é duplicação — é uma exigência da AWS. Certificados do ACM são recursos regionais: só podem ser associados a serviços da mesma região em que foram emitidos. E o CloudFront, por ser global, aceita certificados de uma única região: us-east-1 (Norte da Virgínia). Daí o provider = aws.us_east_1 no segundo certificado, comentado no próprio código.

O certificado principal é emitido na região da aplicação e serve o ALB, que é regional. Se o projeto rodasse em sa-east-1, seriam mesmo dois certificados em duas regiões, para os mesmos nomes.

Como os certificados do ACM são gratuitos e renovados automaticamente, o custo dessa duplicação é zero — o preço é apenas ter que lembrar da regra. Ela costuma aparecer como um erro obscuro de "certificate not found" no terraform apply, quando alguém tenta associar ao CloudFront um certificado de outra região.

Exercício 3

A distribuição declara custom_error_response convertendo 404 e 403 em 200 com /index.html — o padrão para SPA com rotas no cliente. Depois de subir essa configuração, o time do frontend relata que GET /api/pedidos/99999, para um pedido inexistente, deixou de cair no tratamento de erro: a requisição volta com status 200 e um corpo estranho. O que está acontecendo?

Ver resposta

✓ Resposta: O custom_error_response é configurado no nível da distribuição, não do cache behavior. Ele se aplica a todas as origens — inclusive ao ordered_cache_behavior de /api/*, que aponta para o ALB. O 404 legítimo da API é interceptado pelo CloudFront, que devolve ao cliente status 200 e o HTML do index.html. O "corpo estranho" é a página do SPA chegando onde o frontend esperava JSON.

O efeito é pior do que parece: qualquer fetch que verifique response.ok passa a considerar o erro um sucesso, e o JSON.parse falha em cima de <!doctype html> — o erro real fica mascarado por um erro de parsing. E o 403 sofre o mesmo destino: falhas de autorização da API voltam como 200.

A saída mais limpa é a que o próprio artigo já preparou: o registro api.${var.domain_name} aponta direto para o ALB. Servindo a API por esse subdomínio, ela sai do caminho do CloudFront e o custom_error_response volta a valer só para o SPA. Se manter o /api/* na mesma distribuição for um requisito, é preciso uma CloudFront Function na resposta para restaurar o status original quando o caminho começar com /api/.

Exercício 4

O workflow de deploy faz dois aws s3 sync: primeiro dist/assets/, depois dist/ excluindo assets/*. Os dois usam --delete. O que quebra se a ordem for invertida — HTML primeiro, assets depois?

Ver resposta

✓ Resposta: Abre-se uma janela em que o site fica quebrado. O index.html gerado pelo build referencia os assets pelo nome com hash da versão nova (app-a3f9c1.js). Se o HTML sobe primeiro, os navegadores passam a receber a página nova pedindo arquivos que ainda não estão no bucket — todo o JavaScript e CSS retorna 404 e a aplicação não carrega.

O --delete agrava: o primeiro sync do HTML não afeta assets/, mas assim que o segundo sync roda com --delete, os assets da versão anterior são removidos — e qualquer usuário com a página antiga aberta que ainda não baixou um chunk (code splitting, lazy loading de rota) recebe 404 no meio da navegação.

A ordem correta subir-assets-depois-HTML é o que torna o deploy praticamente atômico: enquanto o HTML antigo está no ar, os assets novos já existem sem serem referenciados por ninguém — arquivos de nome novo não colidem com os antigos. A troca acontece no instante em que o HTML muda.

É também por isso que os dois syncs têm Cache-Control opostos: immutable, max-age=31536000 nos assets com hash (o nome muda a cada build, então nunca há o que invalidar) e no-cache, no-store, must-revalidate no HTML, que precisa ser sempre relido para apontar para a versão nova.

Exercício 5

Ainda no workflow, o passo final invalida o cache com --paths "/*" e espera a conclusão com wait invalidation-completed. Considerando os Cache-Control definidos no passo anterior e a aws_cloudfront_cache_policy.assets_estaticos (min_ttl = 0, max_ttl = 31536000), o que essa invalidação realmente está limpando?

Ver resposta

✓ Resposta: Quase nada — ela é, na prática, redundante.

Com min_ttl = 0 e max_ttl alto, a cache policy não força TTL nenhum: ela apenas delimita a faixa em que o Cache-Control vindo da origem é respeitado. Como o S3 devolve no-cache, no-store, must-revalidate nos arquivos HTML, o edge revalida com a origem a cada requisição — não há cópia antiga de HTML para invalidar. E os assets sob /assets/ têm hash no nome: a versão nova está em uma URL que nunca esteve em cache, e a versão antiga sai do cache sozinha por expiração. Invalidar um caminho que ninguém mais vai pedir não muda nada.

O custo direto é pequeno (um /* conta como um único path na cobrança, e os primeiros 1.000 por mês são gratuitos), mas o wait invalidation-completed tem um custo real de pipeline: propagar por todos os edges leva tipicamente de 1 a 5 minutos, adicionados a todo deploy sem contrapartida.

A invalidação passa a ser necessária no caso oposto — quando o HTML é servido com cache no edge (para ganhar latência), ou quando existem arquivos sem hash no nome, como /config.json, /manifest.json ou /robots.txt. Aí o correto é invalidar exatamente esses caminhos, não /*:

aws cloudfront create-invalidation \
  --distribution-id "$DISTRIBUTION_ID" \
  --paths "/index.html" "/config.json" "/manifest.json"
Comentários

Mais em DevOps

O Que é CI/CD e Por Que Sua Empresa Precisa Disso
O Que é CI/CD e Por Que Sua Empresa Precisa Disso

O que é CI/CD e por que ele existe: o integration hell que o antecedeu, a…

Publicando Imagens no Docker Hub e GitHub Container Registry
Publicando Imagens no Docker Hub e GitHub Container Registry

Publicando imagens em registros de container: convenção de nomes no Docker Hub…

Boas Práticas de Imagens: Leveza, Segurança e Camadas
Boas Práticas de Imagens: Leveza, Segurança e Camadas

Os princípios que separam uma imagem funcional de uma imagem de produção…