DevOps

Capstone: Provisionando a Infraestrutura Completa Já leu

17 min de leitura

Capstone: Provisionando a Infraestrutura Completa
Em projetos reais, a infraestrutura raramente é provisionada de uma vez. Ela evolui incrementalmente — começando com o mínimo necessário para rodar a primeira versão e crescendo conforme o sistema e os requisitos amadure

Em projetos reais, a infraestrutura raramente é provisionada de uma vez. Ela evolui incrementalmente — começando com o mínimo necessário para rodar a primeira versão e crescendo conforme o sistema e os requisitos amadurecem. O capstone adota essa abordagem: a infraestrutura é provisionada em camadas, cada uma construída sobre a anterior, com o Terraform como linguagem única de declaração do estado desejado.

A sequência de provisionamento segue a ordem de dependências naturais do sistema: rede primeiro, porque tudo depende dela; segredos e KMS, porque recursos seguros dependem de chaves; banco de dados e cache, porque os serviços dependem dos dados; EKS, porque os pods dependem do cluster; e finalmente os add-ons do cluster, porque dependem do EKS estar operacional.


Camada 1: Rede e Segurança Fundacional

# infrastructure/terraform/modules/networking/main.tf

locals {
  azs             = slice(data.aws_availability_zones.available.names, 0, 3)
  cidr_vpc        = "10.0.0.0/16"

  # Subnets calculadas automaticamente para 3 AZs
  cidrs_publicos  = [for i, az in local.azs : cidrsubnet(local.cidr_vpc, 8, i)]
  cidrs_privados  = [for i, az in local.azs : cidrsubnet(local.cidr_vpc, 8, i + 10)]
  cidrs_dados     = [for i, az in local.azs : cidrsubnet(local.cidr_vpc, 8, i + 20)]
}

data "aws_availability_zones" "available" {
  state = "available"
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.5"

  name = "${var.project_name}-${var.environment}"
  cidr = local.cidr_vpc

  azs              = local.azs
  public_subnets   = local.cidrs_publicos
  private_subnets  = local.cidrs_privados
  database_subnets = local.cidrs_dados

  # NAT Gateways — um por AZ em produção para resiliência
  enable_nat_gateway     = true
  single_nat_gateway     = var.environment != "production"
  one_nat_gateway_per_az = var.environment == "production"

  # DNS interno para service discovery
  enable_dns_hostnames = true
  enable_dns_support   = true

  # Tags necessárias para o EKS descobrir as subnets
  public_subnet_tags = {
    "kubernetes.io/role/elb"                                    = 1
    "kubernetes.io/cluster/${var.project_name}-${var.environment}" = "shared"
  }

  private_subnet_tags = {
    "kubernetes.io/role/internal-elb"                           = 1
    "kubernetes.io/cluster/${var.project_name}-${var.environment}" = "shared"
    "karpenter.sh/discovery"                                    = "${var.project_name}-${var.environment}"
  }

  tags = local.tags_comuns
}

# Security Groups para cada camada da aplicação
resource "aws_security_group" "alb" {
  name        = "${var.project_name}-${var.environment}-alb"
  description = "Security group do Application Load Balancer"
  vpc_id      = module.vpc.vpc_id

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

  ingress {
    description = "HTTP — redirecionado para HTTPS"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    description = "Tráfego para os nós EKS"
    from_port   = 0
    to_port     = 65535
    protocol    = "tcp"
    cidr_blocks = [local.cidr_vpc]
  }

  tags = merge(local.tags_comuns, { Name = "${var.project_name}-${var.environment}-alb" })
}

resource "aws_security_group" "eks_nodes" {
  name        = "${var.project_name}-${var.environment}-eks-nodes"
  description = "Security group dos nós worker do EKS"
  vpc_id      = module.vpc.vpc_id

  ingress {
    description     = "Tráfego do ALB"
    from_port       = 0
    to_port         = 65535
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  ingress {
    description = "Comunicação entre nós"
    from_port   = 0
    to_port     = 65535
    protocol    = "-1"
    self        = true
  }

  egress {
    description = "Todo tráfego de saída"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(local.tags_comuns, { Name = "${var.project_name}-${var.environment}-eks-nodes" })
}

resource "aws_security_group" "rds" {
  name        = "${var.project_name}-${var.environment}-rds"
  description = "Security group do RDS PostgreSQL"
  vpc_id      = module.vpc.vpc_id

  ingress {
    description     = "PostgreSQL dos nós EKS"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.eks_nodes.id]
  }

  tags = merge(local.tags_comuns, { Name = "${var.project_name}-${var.environment}-rds" })
}

resource "aws_security_group" "elasticache" {
  name        = "${var.project_name}-${var.environment}-elasticache"
  description = "Security group do ElastiCache Redis"
  vpc_id      = module.vpc.vpc_id

  ingress {
    description     = "Redis dos nós EKS"
    from_port       = 6379
    to_port         = 6379
    protocol        = "tcp"
    security_groups = [aws_security_group.eks_nodes.id]
  }

  tags = merge(local.tags_comuns, { Name = "${var.project_name}-${var.environment}-elasticache" })
}

Camada 2: KMS e Secrets Manager

# infrastructure/terraform/modules/secrets/main.tf

# Chave KMS mestre — usada para criptografar todos os outros recursos
resource "aws_kms_key" "principal" {
  description             = "Chave KMS principal — ${var.project_name} ${var.environment}"
  deletion_window_in_days = 7
  enable_key_rotation     = true
  multi_region            = false

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AdministracaoRaiz"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        }
        Action   = "kms:*"
        Resource = "*"
      },
      {
        Sid    = "PermitirEKS"
        Effect = "Allow"
        Principal = {
          AWS = module.eks.eks_managed_node_groups["geral"].iam_role_arn
        }
        Action = [
          "kms:Decrypt",
          "kms:GenerateDataKey",
        ]
        Resource = "*"
      }
    ]
  })

  tags = local.tags_comuns
}

resource "aws_kms_alias" "principal" {
  name          = "alias/${var.project_name}-${var.environment}"
  target_key_id = aws_kms_key.principal.key_id
}

# Secrets por serviço — cada serviço tem suas próprias credenciais
locals {
  servicos = ["catalog", "user", "order", "notification", "api-gateway"]
}

resource "aws_secretsmanager_secret" "servico" {
  for_each = toset(local.servicos)

  name        = "${var.project_name}/${var.environment}/${each.key}"
  description = "Credenciais do serviço ${each.key}"
  kms_key_id  = aws_kms_key.principal.arn

  # Rotação automática — requer implementação do Lambda de rotação
  # Habilitado apenas em produção
  dynamic "rotation_rules" {
    for_each = var.environment == "production" ? [1] : []
    content {
      automatically_after_days = 30
    }
  }

  recovery_window_in_days = var.environment == "production" ? 7 : 0

  tags = merge(local.tags_comuns, { Servico = each.key })
}

# Valores iniciais dos secrets — substituídos em produção por pipeline seguro
resource "aws_secretsmanager_secret_version" "servico" {
  for_each = toset(local.servicos)

  secret_id = aws_secretsmanager_secret.servico[each.key].id

  secret_string = jsonencode({
    DATABASE_URL = each.key != "notification" ? (
      "postgresql://${each.key}_user:SUBSTITUIR_EM_PRODUCAO@${
        aws_route53_record.rds[each.key].fqdn
      }:5432/${each.key}_db"
    ) : null
    REDIS_URL    = "rediss://:SUBSTITUIR_EM_PRODUCAO@${
      aws_elasticache_replication_group.redis.primary_endpoint_address
    }:6379"
    JWT_SECRET   = "SUBSTITUIR_EM_PRODUCAO"
  })

  lifecycle {
    # Nunca sobrescreve valores que foram atualizados manualmente
    ignore_changes = [secret_string]
  }
}

Camada 3: Banco de Dados por Serviço

# infrastructure/terraform/modules/databases/main.tf

locals {
  # Serviços que precisam de banco de dados próprio
  servicos_com_db = {
    catalog = {
      classe_staging    = "db.t3.small"
      classe_producao   = "db.r6g.large"
      storage_inicial   = 20
      storage_maximo    = 500
    }
    user = {
      classe_staging    = "db.t3.small"
      classe_producao   = "db.r6g.large"
      storage_inicial   = 20
      storage_maximo    = 200
    }
    order = {
      classe_staging    = "db.t3.medium"
      classe_producao   = "db.r6g.xlarge"  # Maior — core do negócio
      storage_inicial   = 50
      storage_maximo    = 2000
    }
  }
}

# Subnet group compartilhado para todos os bancos
resource "aws_db_subnet_group" "principal" {
  name       = "${var.project_name}-${var.environment}"
  subnet_ids = module.vpc.database_subnets

  tags = local.tags_comuns
}

# Parameter group otimizado para produção
resource "aws_db_parameter_group" "postgres16" {
  name   = "${var.project_name}-${var.environment}-pg16"
  family = "postgres16"

  parameter {
    name  = "shared_buffers"
    value = "{DBInstanceClassMemory/4096}"
  }

  parameter {
    name  = "log_min_duration_statement"
    value = var.environment == "production" ? "1000" : "100"
  }

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

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

  parameter {
    name  = "max_connections"
    value = "200"
  }

  lifecycle {
    create_before_destroy = true
  }
}

# Um banco RDS por serviço
resource "aws_db_instance" "servico" {
  for_each = local.servicos_com_db

  identifier = "${var.project_name}-${var.environment}-${each.key}"

  engine               = "postgres"
  engine_version       = "16.1"
  instance_class       = var.environment == "production" ? (
    each.value.classe_producao
  ) : each.value.classe_staging

  allocated_storage     = each.value.storage_inicial
  max_allocated_storage = each.value.storage_maximo
  storage_type          = "gp3"
  storage_encrypted     = true
  kms_key_id            = aws_kms_key.principal.arn

  db_name  = "${replace(each.key, "-", "_")}_db"
  username = "${replace(each.key, "-", "_")}_admin"

  manage_master_user_password   = true
  master_user_secret_kms_key_id = aws_kms_key.principal.arn

  multi_az               = var.environment == "production"
  db_subnet_group_name   = aws_db_subnet_group.principal.name
  vpc_security_group_ids = [aws_security_group.rds.id]
  publicly_accessible    = false
  parameter_group_name   = aws_db_parameter_group.postgres16.name

  backup_retention_period = var.environment == "production" ? 14 : 3
  backup_window           = "02:00-03:00"
  maintenance_window      = "Mon:03:00-Mon:04:00"

  auto_minor_version_upgrade = true
  deletion_protection        = var.environment == "production"
  skip_final_snapshot        = var.environment != "production"

  final_snapshot_identifier = var.environment == "production" ? (
    "${var.project_name}-${var.environment}-${each.key}-final"
  ) : null

  performance_insights_enabled          = true
  performance_insights_retention_period = 7
  monitoring_interval                   = 60
  monitoring_role_arn                   = aws_iam_role.rds_monitoring.arn

  enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]

  tags = merge(local.tags_comuns, {
    Servico = each.key
    DBName  = "${replace(each.key, "-", "_")}_db"
  })
}

# Read Replica para o serviço de catálogo — alta taxa de leitura
resource "aws_db_instance" "catalog_replica" {
  count = var.environment == "production" ? 1 : 0

  identifier             = "${var.project_name}-${var.environment}-catalog-replica"
  replicate_source_db    = aws_db_instance.servico["catalog"].identifier
  instance_class         = "db.r6g.large"
  storage_encrypted      = true
  publicly_accessible    = false
  vpc_security_group_ids = [aws_security_group.rds.id]
  parameter_group_name   = aws_db_parameter_group.postgres16.name

  backup_retention_period    = 0
  skip_final_snapshot        = true
  auto_minor_version_upgrade = true
  performance_insights_enabled = true

  tags = merge(local.tags_comuns, {
    Servico = "catalog"
    Role    = "read-replica"
  })
}

# Registros DNS internos para abstrair os endpoints
resource "aws_route53_zone" "interna" {
  name = "interno.${var.domain_name}"

  vpc {
    vpc_id = module.vpc.vpc_id
  }

  tags = local.tags_comuns
}

resource "aws_route53_record" "rds" {
  for_each = local.servicos_com_db

  zone_id = aws_route53_zone.interna.zone_id
  name    = "${each.key}-db.interno.${var.domain_name}"
  type    = "CNAME"
  ttl     = 60
  records = [aws_db_instance.servico[each.key].address]
}

Camada 4: ElastiCache e Filas

# infrastructure/terraform/modules/messaging/main.tf

# Redis compartilhado para cache dos serviços
resource "aws_elasticache_subnet_group" "redis" {
  name       = "${var.project_name}-${var.environment}-redis"
  subnet_ids = module.vpc.private_subnets
  tags       = local.tags_comuns
}

resource "aws_elasticache_parameter_group" "redis7" {
  name   = "${var.project_name}-${var.environment}-redis7"
  family = "redis7"

  parameter {
    name  = "maxmemory-policy"
    value = "allkeys-lru"  # Remove chaves menos usadas quando memória esgota
  }

  parameter {
    name  = "notify-keyspace-events"
    value = "Ex"
  }
}

resource "aws_elasticache_replication_group" "redis" {
  replication_group_id = "${var.project_name}-${var.environment}"
  description          = "Cache Redis — ${var.project_name} ${var.environment}"

  node_type            = var.environment == "production" ? "cache.r6g.large" : "cache.t3.micro"
  num_cache_clusters   = var.environment == "production" ? 3 : 1
  port                 = 6379
  parameter_group_name = aws_elasticache_parameter_group.redis7.name
  subnet_group_name    = aws_elasticache_subnet_group.redis.name
  security_group_ids   = [aws_security_group.elasticache.id]

  engine_version              = "7.1"
  automatic_failover_enabled  = var.environment == "production"
  multi_az_enabled            = var.environment == "production"
  at_rest_encryption_enabled  = true
  transit_encryption_enabled  = true
  auth_token                  = random_password.redis_auth.result
  auth_token_update_strategy  = "ROTATE"

  snapshot_window          = "02:00-03:00"
  snapshot_retention_limit = var.environment == "production" ? 7 : 1
  maintenance_window       = "tue:03:00-tue:04:00"

  log_delivery_configuration {
    destination      = aws_cloudwatch_log_group.redis_slow.name
    destination_type = "cloudwatch-logs"
    log_format       = "json"
    log_type         = "slow-log"
  }

  tags = local.tags_comuns
}

resource "random_password" "redis_auth" {
  length  = 32
  special = false  # Redis auth token não suporta alguns caracteres especiais
}

# Armazena o token do Redis no Secrets Manager
resource "aws_secretsmanager_secret_version" "redis_auth" {
  secret_id     = aws_secretsmanager_secret.redis_auth.id
  secret_string = random_password.redis_auth.result
}

resource "aws_secretsmanager_secret" "redis_auth" {
  name       = "${var.project_name}/${var.environment}/redis-auth-token"
  kms_key_id = aws_kms_key.principal.arn
  tags       = local.tags_comuns
}

# ── Filas SQS ──────────────────────────────────────────────────────

locals {
  filas = {
    "pedido-confirmado" = {
      descricao   = "Notifica serviços quando pedido é confirmado"
      retencao    = 86400   # 1 dia
      visibilidade = 30
      max_receives = 3
    }
    "pedido-cancelado" = {
      descricao   = "Notifica serviços quando pedido é cancelado"
      retencao    = 86400
      visibilidade = 30
      max_receives = 3
    }
    "estoque-atualizado" = {
      descricao   = "Notifica catálogo quando estoque muda"
      retencao    = 3600    # 1 hora — baixa prioridade
      visibilidade = 60
      max_receives = 5
    }
  }
}

# Dead Letter Queues — capturam mensagens que falharam repetidamente
resource "aws_sqs_queue" "dlq" {
  for_each = local.filas

  name                      = "${var.project_name}-${var.environment}-${each.key}-dlq"
  message_retention_seconds = 1209600  # 14 dias para investigação
  kms_master_key_id         = aws_kms_key.principal.arn

  tags = merge(local.tags_comuns, {
    Fila = each.key
    Tipo = "dlq"
  })
}

# Filas principais com referência às DLQs
resource "aws_sqs_queue" "principal" {
  for_each = local.filas

  name                       = "${var.project_name}-${var.environment}-${each.key}"
  message_retention_seconds  = each.value.retencao
  visibility_timeout_seconds = each.value.visibilidade
  kms_master_key_id          = aws_kms_key.principal.arn

  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.dlq[each.key].arn
    maxReceiveCount     = each.value.max_receives
  })

  tags = merge(local.tags_comuns, {
    Fila      = each.key
    Descricao = each.value.descricao
  })
}

# Alarmes para mensagens acumulando nas DLQs
resource "aws_cloudwatch_metric_alarm" "dlq_mensagens" {
  for_each = local.filas

  alarm_name          = "${var.project_name}-${var.environment}-dlq-${each.key}"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  metric_name         = "ApproximateNumberOfMessagesVisible"
  namespace           = "AWS/SQS"
  period              = 300
  statistic           = "Sum"
  threshold           = 0  # Qualquer mensagem na DLQ é alerta

  dimensions = {
    QueueName = aws_sqs_queue.dlq[each.key].name
  }

  alarm_description = "Mensagens na DLQ ${each.key} — investigar falhas de processamento"
  alarm_actions     = [aws_sns_topic.alertas.arn]

  tags = local.tags_comuns
}

Camada 5: Cluster EKS

# infrastructure/terraform/modules/eks/main.tf

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

  cluster_name    = "${var.project_name}-${var.environment}"
  cluster_version = "1.29"

  vpc_id                    = module.vpc.vpc_id
  subnet_ids                = module.vpc.private_subnets
  control_plane_subnet_ids  = module.vpc.private_subnets

  cluster_endpoint_public_access       = true
  cluster_endpoint_public_access_cidrs = var.acesso_publico_cidrs
  cluster_endpoint_private_access      = true

  cluster_encryption_config = {
    provider_key_arn = aws_kms_key.principal.arn
    resources        = ["secrets"]
  }

  cluster_addons = {
    coredns                = { most_recent = true }
    kube-proxy             = { most_recent = true }
    vpc-cni = {
      most_recent              = true
      service_account_role_arn = module.vpc_cni_irsa.iam_role_arn
    }
    aws-ebs-csi-driver = {
      most_recent              = true
      service_account_role_arn = module.ebs_csi_irsa.iam_role_arn
    }
  }

  eks_managed_node_groups = {
    # Node group de sistema — para add-ons e workloads da plataforma
    sistema = {
      name            = "${var.project_name}-${var.environment}-sistema"
      instance_types  = ["m6i.large"]
      capacity_type   = "ON_DEMAND"
      min_size        = 2
      max_size        = 4
      desired_size    = 2

      labels = { role = "sistema" }

      taints = [{
        key    = "CriticalAddonsOnly"
        value  = "true"
        effect = "NO_SCHEDULE"
      }]

      block_device_mappings = {
        xvda = {
          device_name = "/dev/xvda"
          ebs = {
            volume_size           = 50
            volume_type           = "gp3"
            encrypted             = true
            kms_key_id            = aws_kms_key.eks_nodes.arn
            delete_on_termination = true
          }
        }
      }
    }

    # Node group de aplicação — para os microsserviços
    aplicacao = {
      name           = "${var.project_name}-${var.environment}-aplicacao"
      instance_types = ["m6i.large", "m6a.large", "m7i.large"]
      capacity_type  = var.environment == "production" ? "ON_DEMAND" : "SPOT"
      min_size       = var.environment == "production" ? 3 : 1
      max_size       = 20
      desired_size   = var.environment == "production" ? 3 : 1

      labels = { role = "aplicacao" }

      block_device_mappings = {
        xvda = {
          device_name = "/dev/xvda"
          ebs = {
            volume_size           = 50
            volume_type           = "gp3"
            encrypted             = true
            kms_key_id            = aws_kms_key.eks_nodes.arn
            delete_on_termination = true
          }
        }
      }
    }
  }

  # Acesso ao cluster via IAM Identity Center
  access_entries = {
    admin = {
      principal_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/AWSReservedSSO_AWSAdministratorAccess_${var.sso_suffix}"
      policy_associations = {
        admin = {
          policy_arn   = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
          access_scope = { type = "cluster" }
        }
      }
    }
    pipeline = {
      principal_arn = aws_iam_role.pipeline_deploy.arn
      policy_associations = {
        editor = {
          policy_arn   = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSEditPolicy"
          access_scope = {
            type       = "namespace"
            namespaces = ["producao", "staging"]
          }
        }
      }
    }
  }

  tags = local.tags_comuns
}

resource "aws_kms_key" "eks_nodes" {
  description             = "KMS para volumes EBS dos nós EKS"
  deletion_window_in_days = 7
  enable_key_rotation     = true
  tags                    = local.tags_comuns
}

Camada 6: Namespaces e Configurações do Cluster

# infrastructure/terraform/modules/kubernetes-base/main.tf

# Namespaces com labels de Pod Security Standards
resource "kubernetes_namespace" "producao" {
  metadata {
    name = "producao"

    labels = {
      "pod-security.kubernetes.io/enforce"         = "restricted"
      "pod-security.kubernetes.io/enforce-version" = "latest"
      "pod-security.kubernetes.io/audit"           = "restricted"
      "pod-security.kubernetes.io/warn"            = "restricted"
    }

    annotations = {
      "scheduler.alpha.kubernetes.io/node-selector" = "role=aplicacao"
    }
  }
}

resource "kubernetes_namespace" "monitoring" {
  metadata {
    name = "monitoring"

    labels = {
      "pod-security.kubernetes.io/enforce" = "baseline"
    }
  }
}

# Resource Quotas por namespace
resource "kubernetes_resource_quota" "producao" {
  metadata {
    name      = "quota-producao"
    namespace = kubernetes_namespace.producao.metadata[0].name
  }

  spec {
    hard = {
      "requests.cpu"    = "20"
      "requests.memory" = "40Gi"
      "limits.cpu"      = "40"
      "limits.memory"   = "80Gi"
      "pods"            = "100"
    }
  }
}

# LimitRange — define padrões de resources para pods sem especificação
resource "kubernetes_limit_range" "producao" {
  metadata {
    name      = "limitrange-producao"
    namespace = kubernetes_namespace.producao.metadata[0].name
  }

  spec {
    limit {
      type = "Container"

      default = {
        cpu    = "500m"
        memory = "512Mi"
      }

      default_request = {
        cpu    = "100m"
        memory = "128Mi"
      }

      max = {
        cpu    = "4"
        memory = "4Gi"
      }
    }
  }
}

# StorageClass gp3 como padrão
resource "kubernetes_storage_class" "gp3" {
  metadata {
    name = "gp3"
    annotations = {
      "storageclass.kubernetes.io/is-default-class" = "true"
    }
  }

  storage_provisioner    = "ebs.csi.aws.com"
  volume_binding_mode    = "WaitForFirstConsumer"
  allow_volume_expansion = true

  parameters = {
    type      = "gp3"
    iops      = "3000"
    throughput = "125"
    encrypted = "true"
    kmsKeyId  = aws_kms_key.principal.arn
  }
}

# Add-ons via Helm
resource "helm_release" "aws_load_balancer_controller" {
  name       = "aws-load-balancer-controller"
  repository = "https://aws.github.io/eks-charts"
  chart      = "aws-load-balancer-controller"
  namespace  = "kube-system"
  version    = "1.7.1"

  set {
    name  = "clusterName"
    value = module.eks.cluster_name
  }

  set {
    name  = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"
    value = module.alb_controller_irsa.iam_role_arn
  }
}

resource "helm_release" "external_secrets" {
  name             = "external-secrets"
  repository       = "https://charts.external-secrets.io"
  chart            = "external-secrets"
  namespace        = "external-secrets"
  create_namespace = true
  version          = "0.9.13"

  set {
    name  = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"
    value = module.external_secrets_irsa.iam_role_arn
  }

  depends_on = [module.eks]
}

resource "helm_release" "argocd" {
  name             = "argocd"
  repository       = "https://argoproj.github.io/argo-helm"
  chart            = "argo-cd"
  namespace        = "argocd"
  create_namespace = true
  version          = "6.4.0"

  values = [file("${path.module}/argocd-values.yaml")]

  depends_on = [module.eks]
}

resource "helm_release" "kube_prometheus_stack" {
  name             = "kube-prometheus-stack"
  repository       = "https://prometheus-community.github.io/helm-charts"
  chart            = "kube-prometheus-stack"
  namespace        = "monitoring"
  version          = "56.6.2"

  values = [file("${path.module}/prometheus-values.yaml")]

  depends_on = [kubernetes_namespace.monitoring]
}

Outputs da Infraestrutura

# infrastructure/terraform/environments/production/outputs.tf

output "cluster_name" {
  description = "Nome do cluster EKS"
  value       = module.eks.cluster_name
}

output "cluster_endpoint" {
  description = "Endpoint da API do EKS"
  value       = module.eks.cluster_endpoint
  sensitive   = true
}

output "vpc_id" {
  description = "ID da VPC"
  value       = module.vpc.vpc_id
}

output "rds_endpoints" {
  description = "Endpoints dos bancos de dados por serviço"
  value = {
    for servico, db in aws_db_instance.servico :
    servico => db.address
  }
  sensitive = true
}

output "redis_endpoint" {
  description = "Endpoint primário do Redis"
  value       = aws_elasticache_replication_group.redis.primary_endpoint_address
  sensitive   = true
}

output "sqs_urls" {
  description = "URLs das filas SQS"
  value = {
    for nome, fila in aws_sqs_queue.principal :
    nome => fila.url
  }
}

output "secrets_arns" {
  description = "ARNs dos secrets no Secrets Manager por serviço"
  value = {
    for servico, secret in aws_secretsmanager_secret.servico :
    servico => secret.arn
  }
  sensitive = true
}

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

Executando o Provisionamento

#!/bin/bash
# scripts/provisionar-infraestrutura.sh
# Provisiona a infraestrutura completa na ordem correta de dependências

set -euo pipefail

AMBIENTE="${1:?Uso: $0 <staging|production>}"
REGIAO="us-east-1"

log() { echo "[$(date -u +%H:%M:%S)] $*"; }

cd infrastructure/terraform/environments/$AMBIENTE

log "=== Provisionando infraestrutura — ambiente: $AMBIENTE ==="

# Inicializa o Terraform com backend remoto
log "Inicializando Terraform..."
terraform init \
  -backend-config="bucket=${TF_STATE_BUCKET}" \
  -backend-config="key=${AMBIENTE}/terraform.tfstate" \
  -backend-config="region=${REGIAO}" \
  -backend-config="dynamodb_table=${TF_LOCK_TABLE}"

# Valida a configuração
log "Validando configuração..."
terraform validate

# Plano com saída para arquivo
log "Gerando plano..."
terraform plan \
  -var="environment=${AMBIENTE}" \
  -var="aws_region=${REGIAO}" \
  -out=tfplan

# Em produção, requer aprovação manual antes de aplicar
if [ "$AMBIENTE" = "production" ]; then
  echo ""
  echo "⚠️  ATENÇÃO: Aplicando mudanças em PRODUÇÃO"
  echo "Revise o plano acima cuidadosamente."
  read -rp "Digite 'aplicar' para confirmar: " confirmacao
  if [ "$confirmacao" != "aplicar" ]; then
    log "Operação cancelada pelo usuário"
    exit 0
  fi
fi

log "Aplicando mudanças..."
terraform apply tfplan

# Atualiza o kubeconfig
log "Atualizando kubeconfig..."
aws eks update-kubeconfig \
  --name "$(terraform output -raw cluster_name)" \
  --region "$REGIAO"

log "✅ Infraestrutura provisionada com sucesso!"
log "Cluster: $(terraform output -raw cluster_name)"

O Que Vem a Seguir

Com a infraestrutura provisionada, o próximo artigo implementa os cinco microsserviços: código funcional com todas as práticas de resiliência, observabilidade e segurança integradas, Dockerfiles otimizados, manifestos Kubernetes completos e a configuração do ArgoCD para deploy declarativo.


Referências para Aprofundamento

Terraform e IaC - Terraform Best Practices — developer.hashicorp.com — Guia oficial de boas práticas para estruturação de módulos Terraform, incluindo convenções de nomenclatura, composição de módulos e estratégias de versionamento. - AWS VPC Terraform Module — registry.terraform.io — Documentação do módulo oficial de VPC para AWS com todos os parâmetros disponíveis e exemplos de uso para diferentes arquiteturas de rede.

EKS - EKS Best Practices Guide — aws.github.io — Guia oficial de boas práticas para EKS mantido pela AWS, cobrindo segurança, networking, escalabilidade e monitoramento com recomendações detalhadas para cada área. - EKS Workshop — eksworkshop.com — Workshop hands-on oficial da AWS para EKS com laboratórios práticos cobrindo desde a criação do cluster até práticas avançadas de segurança e observabilidade.

Comentários

Mais em DevOps

Grafana: Dashboards e Alertas que Fazem Sentido
Grafana: Dashboards e Alertas que Fazem Sentido

Um dashboard mal projetado é quase tão ruim quanto não ter dashboard. Quando...

Pipeline com GitHub Actions: Build, Test e Deploy Automático
Pipeline com GitHub Actions: Build, Test e Deploy Automático

O artigo anterior estabeleceu os fundamentos conceituais do CI/CD. Este artig...

Cultura DevOps: Maturidade, Postmortems e Melhoria Contínua
Cultura DevOps: Maturidade, Postmortems e Melhoria Contínua

É possível instalar todas as ferramentas descritas nos artigos anteriores — K...