DevOps

EKS: Kubernetes Gerenciado na AWS Já leu

13 min de leitura

EKS: Kubernetes Gerenciado na AWS
Operar o plano de controle do Kubernetes — manter o etcd replicado, atualizar os componentes do control plane, garantir sua disponibilidade, monitorar sua saúde — é um trabalho de tempo integral que exige profundo conhec

Operar o plano de controle do Kubernetes — manter o etcd replicado, atualizar os componentes do control plane, garantir sua disponibilidade, monitorar sua saúde — é um trabalho de tempo integral que exige profundo conhecimento do sistema. Para a maioria das organizações, esse esforço operacional não gera vantagem competitiva: o negócio se beneficia de ter aplicações bem orquestradas, não de operar o orquestrador em si.

O Amazon Elastic Kubernetes Service — EKS — resolve esse problema gerenciando o plano de controle da AWS. A AWS garante a disponibilidade do kube-apiserver, do etcd e dos demais componentes do control plane, aplica patches de segurança e disponibiliza novas versões do Kubernetes. O time de engenharia mantém o controle sobre os nós worker, os workloads e a configuração do cluster — sem a responsabilidade de operar o que está abaixo.

O EKS é compatível com todas as ferramentas e manifestos Kubernetes padrão. Um cluster EKS aceita os mesmos YAMLs que rodam em um cluster local criado com kind ou kubeadm — a portabilidade é uma das promessas fundamentais do Kubernetes.


Provisionando um Cluster EKS com Terraform

A criação de um cluster EKS envolve múltiplos componentes: o próprio cluster, os node groups, as configurações de rede e as permissões IAM. O módulo oficial terraform-aws-modules/eks/aws encapsula essa complexidade:

# eks.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.31"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.25"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.12"
    }
  }
}

# Dados do cluster após criação — necessários para configurar providers
data "aws_eks_cluster" "principal" {
  name = module.eks.cluster_name
}

data "aws_eks_cluster_auth" "principal" {
  name = module.eks.cluster_name
}

# Provider Kubernetes aponta para o cluster EKS
provider "kubernetes" {
  host                   = data.aws_eks_cluster.principal.endpoint
  cluster_ca_certificate = base64decode(
    data.aws_eks_cluster.principal.certificate_authority[0].data
  )
  token = data.aws_eks_cluster_auth.principal.token
}

provider "helm" {
  kubernetes {
    host                   = data.aws_eks_cluster.principal.endpoint
    cluster_ca_certificate = base64decode(
      data.aws_eks_cluster.principal.certificate_authority[0].data
    )
    token = data.aws_eks_cluster_auth.principal.token
  }
}

# Módulo EKS — provisiona cluster e node groups
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

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

  # Rede
  vpc_id                   = module.vpc.vpc_id
  subnet_ids               = module.vpc.ids_subnets_privadas
  control_plane_subnet_ids = module.vpc.ids_subnets_privadas

  # Acesso à API pública — restringe por CIDR
  cluster_endpoint_public_access       = true
  cluster_endpoint_public_access_cidrs = var.eks_public_access_cidrs
  cluster_endpoint_private_access      = true

  # Criptografia dos secrets do etcd com KMS
  cluster_encryption_config = {
    provider_key_arn = aws_kms_key.eks.arn
    resources        = ["secrets"]
  }

  # Add-ons gerenciados pela AWS
  cluster_addons = {
    coredns = {
      most_recent = true
      configuration_values = jsonencode({
        replicaCount = 2
        resources = {
          requests = { cpu = "100m", memory = "70Mi" }
          limits   = { memory = "170Mi" }
        }
      })
    }
    kube-proxy = {
      most_recent = true
    }
    vpc-cni = {
      most_recent              = true
      service_account_role_arn = module.vpc_cni_irsa.iam_role_arn
      configuration_values = jsonencode({
        enableNetworkPolicy = "true"
      })
    }
    aws-ebs-csi-driver = {
      most_recent              = true
      service_account_role_arn = module.ebs_csi_irsa.iam_role_arn
    }
  }

  # Node Groups gerenciados
  eks_managed_node_groups = {
    # Node group para cargas de trabalho gerais
    geral = {
      name            = "${var.project_name}-${var.environment}-geral"
      instance_types  = ["m6i.large", "m6a.large", "m7i.large"]
      capacity_type   = "ON_DEMAND"
      min_size        = 2
      max_size        = 10
      desired_size    = 3

      # Spread entre AZs para resiliência
      subnet_ids = module.vpc.ids_subnets_privadas

      labels = {
        role        = "geral"
        environment = var.environment
      }

      taints = []

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

      update_config = {
        max_unavailable_percentage = 33
      }

      tags = local.tags_comuns
    }

    # Node group para workloads com GPU (ML/inferência)
    gpu = {
      name           = "${var.project_name}-${var.environment}-gpu"
      instance_types = ["g4dn.xlarge"]
      capacity_type  = "SPOT"
      min_size       = 0
      max_size       = 5
      desired_size   = 0

      labels = {
        role             = "gpu"
        "nvidia.com/gpu" = "true"
      }

      taints = [{
        key    = "nvidia.com/gpu"
        value  = "true"
        effect = "NO_SCHEDULE"
      }]

      tags = local.tags_comuns
    }
  }

  # Acesso ao cluster — administradores
  access_entries = {
    admin = {
      kubernetes_groups = []
      principal_arn     = "arn:aws:iam::${data.aws_caller_identity.atual.account_id}:role/eks-admin"
      policy_associations = {
        admin = {
          policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
          access_scope = {
            type = "cluster"
          }
        }
      }
    }
  }

  tags = local.tags_comuns
}

# KMS para criptografia dos secrets do etcd
resource "aws_kms_key" "eks" {
  description             = "KMS key para secrets do cluster EKS"
  deletion_window_in_days = 7
  enable_key_rotation     = true
  tags                    = local.tags_comuns
}

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

IRSA: IAM Roles for Service Accounts

O IRSA — IAM Roles for Service Accounts — é o mecanismo que permite que pods no EKS assumam IAM Roles sem precisar de chaves de acesso estáticas. Funciona via OIDC: o cluster EKS age como um provedor de identidade OIDC, emitindo tokens JWT para as service accounts dos pods. A AWS verifica esses tokens e assume a IAM Role correspondente.

# irsa.tf

# OIDC Provider do cluster EKS
data "tls_certificate" "eks" {
  url = module.eks.cluster_oidc_issuer_url
}

resource "aws_iam_openid_connect_provider" "eks" {
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = [data.tls_certificate.eks.certificates[0].sha1_fingerprint]
  url             = module.eks.cluster_oidc_issuer_url

  tags = local.tags_comuns
}

# IRSA para o VPC CNI — gerencia ENIs para os pods
module "vpc_cni_irsa" {
  source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"

  role_name             = "${var.project_name}-${var.environment}-vpc-cni"
  attach_vpc_cni_policy = true
  vpc_cni_enable_ipv4   = true

  oidc_providers = {
    main = {
      provider_arn               = aws_iam_openid_connect_provider.eks.arn
      namespace_service_accounts = ["kube-system:aws-node"]
    }
  }
}

# IRSA para o EBS CSI Driver — gerencia volumes EBS
module "ebs_csi_irsa" {
  source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"

  role_name             = "${var.project_name}-${var.environment}-ebs-csi"
  attach_ebs_csi_policy = true

  oidc_providers = {
    main = {
      provider_arn               = aws_iam_openid_connect_provider.eks.arn
      namespace_service_accounts = ["kube-system:ebs-csi-controller-sa"]
    }
  }
}

# IRSA para a aplicação — acesso ao S3 e Secrets Manager
module "minha_api_irsa" {
  source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"

  role_name = "${var.project_name}-${var.environment}-minha-api"

  oidc_providers = {
    main = {
      provider_arn               = aws_iam_openid_connect_provider.eks.arn
      namespace_service_accounts = ["producao:minha-api"]
    }
  }
}

resource "aws_iam_role_policy" "minha_api" {
  name = "permissoes-minha-api"
  role = module.minha_api_irsa.iam_role_name

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
        Resource = "${aws_s3_bucket.assets.arn}/*"
      },
      {
        Effect   = "Allow"
        Action   = ["secretsmanager:GetSecretValue"]
        Resource = "arn:aws:secretsmanager:${var.aws_region}:*:secret:${var.project_name}/*"
      }
    ]
  })
}

A Service Account que referencia a IAM Role:

# kubernetes/service-account.yml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: minha-api
  namespace: producao
  annotations:
    # Referencia a IAM Role via IRSA
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/minha-api-producao-minha-api
    eks.amazonaws.com/token-expiration: "86400"

O Deployment que usa a Service Account:

spec:
  template:
    spec:
      serviceAccountName: minha-api
      # Não é necessário configurar credenciais AWS —
      # o SDK detecta automaticamente via IRSA
      containers:
        - name: api
          image: ghcr.io/empresa/minha-api:1.5.0

Karpenter: Auto Scaling Inteligente de Nós

O Karpenter é o provisionador de nós recomendado para EKS — mais rápido e eficiente que o Cluster Autoscaler tradicional. Enquanto o Cluster Autoscaler escala node groups inteiros, o Karpenter provisiona nós individualmente com o tipo exato que melhor atende às necessidades dos pods pendentes.

# karpenter.tf

# IRSA para o Karpenter
module "karpenter_irsa" {
  source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"

  role_name                          = "${var.project_name}-${var.environment}-karpenter"
  attach_karpenter_controller_policy = true

  karpenter_controller_cluster_name       = module.eks.cluster_name
  karpenter_controller_node_iam_role_arns = [module.eks.eks_managed_node_groups["geral"].iam_role_arn]

  oidc_providers = {
    main = {
      provider_arn               = aws_iam_openid_connect_provider.eks.arn
      namespace_service_accounts = ["kube-system:karpenter"]
    }
  }
}

# Instala o Karpenter via Helm
resource "helm_release" "karpenter" {
  namespace        = "kube-system"
  name             = "karpenter"
  repository       = "oci://public.ecr.aws/karpenter"
  chart            = "karpenter"
  version          = "0.34.0"
  create_namespace = false

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

  set {
    name  = "settings.interruptionQueue"
    value = aws_sqs_queue.karpenter.name
  }

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

  set {
    name  = "controller.resources.requests.cpu"
    value = "100m"
  }

  set {
    name  = "controller.resources.requests.memory"
    value = "256Mi"
  }

  set {
    name  = "controller.resources.limits.memory"
    value = "512Mi"
  }
}

# Fila SQS para notificações de interrupção de instâncias Spot
resource "aws_sqs_queue" "karpenter" {
  name                      = "${var.project_name}-${var.environment}-karpenter"
  message_retention_seconds = 300
  sqs_managed_sse_enabled   = true
  tags                      = local.tags_comuns
}

Com o Karpenter instalado, configura-se um NodePool — o conjunto de regras que define quais tipos de nós podem ser provisionados:

# kubernetes/karpenter/nodepool.yml
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: geral
spec:
  template:
    metadata:
      labels:
        role: geral
    spec:
      nodeClassRef:
        apiVersion: karpenter.k8s.aws/v1beta1
        kind: EC2NodeClass
        name: geral

      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64", "arm64"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["m", "c", "r"]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ["5"]

      # Expira nós após 720h para forçar renovação com AMIs atualizadas
      expireAfter: 720h

  limits:
    cpu: "200"
    memory: "800Gi"

  disruption:
    consolidationPolicy: WhenUnderutilized
    consolidateAfter: 30s
---
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
  name: geral
spec:
  amiFamily: AL2
  role: "${var.project_name}-${var.environment}-node"

  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${var.project_name}-${var.environment}"

  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${var.project_name}-${var.environment}"

  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 50Gi
        volumeType: gp3
        iops: 3000
        encrypted: true
        deleteOnTermination: true

  tags:
    Name: "${var.project_name}-${var.environment}-karpenter-node"
    Environment: "${var.environment}"

Instalando Add-ons Essenciais com Helm

Um cluster EKS em produção precisa de uma série de componentes adicionais instalados via Helm:

# addons.tf

# AWS Load Balancer Controller — cria ALBs a partir de Ingress resources
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.aws_load_balancer_controller_irsa.iam_role_arn
  }

  set {
    name  = "replicaCount"
    value = "2"
  }
}

# External Secrets Operator — sincroniza secrets do Secrets Manager para K8s
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
  }
}

# Metrics Server — necessário para HPA funcionar
resource "helm_release" "metrics_server" {
  name       = "metrics-server"
  repository = "https://kubernetes-sigs.github.io/metrics-server/"
  chart      = "metrics-server"
  namespace  = "kube-system"
  version    = "3.11.0"
}

# Prometheus Stack — monitoramento completo do cluster
resource "helm_release" "prometheus_stack" {
  name             = "kube-prometheus-stack"
  repository       = "https://prometheus-community.github.io/helm-charts"
  chart            = "kube-prometheus-stack"
  namespace        = "monitoring"
  create_namespace = true
  version          = "56.6.2"

  values = [
    yamlencode({
      grafana = {
        enabled        = true
        adminPassword  = var.grafana_password
        ingress = {
          enabled          = true
          ingressClassName = "alb"
          annotations = {
            "alb.ingress.kubernetes.io/scheme"      = "internet-facing"
            "alb.ingress.kubernetes.io/target-type" = "ip"
          }
          hosts = ["grafana.${var.domain_name}"]
        }
      }
      prometheus = {
        prometheusSpec = {
          retention            = "15d"
          storageSpec = {
            volumeClaimTemplate = {
              spec = {
                storageClassName = "gp3"
                accessModes      = ["ReadWriteOnce"]
                resources = {
                  requests = { storage = "50Gi" }
                }
              }
            }
          }
        }
      }
      alertmanager = {
        alertmanagerSpec = {
          storage = {
            volumeClaimTemplate = {
              spec = {
                storageClassName = "gp3"
                accessModes      = ["ReadWriteOnce"]
                resources = {
                  requests = { storage = "10Gi" }
                }
              }
            }
          }
        }
      }
    })
  ]
}

External Secrets: Sincronizando Secrets do AWS Secrets Manager

Com o External Secrets Operator instalado, secrets do AWS Secrets Manager são automaticamente sincronizados como Kubernetes Secrets — sem armazená-los em repositórios Git ou gerenciá-los manualmente:

# kubernetes/external-secrets/cluster-secret-store.yml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: aws-secrets-manager
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets
            namespace: external-secrets
---
# kubernetes/external-secrets/minha-api-secrets.yml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: minha-api-secrets
  namespace: producao
spec:
  refreshInterval: 1h

  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore

  target:
    name: minha-api-secrets
    creationPolicy: Owner
    template:
      type: Opaque

  data:
    - secretKey: database-url
      remoteRef:
        key: minha-api/producao
        property: DATABASE_URL

    - secretKey: redis-url
      remoteRef:
        key: minha-api/producao
        property: REDIS_URL

    - secretKey: jwt-secret
      remoteRef:
        key: minha-api/producao
        property: JWT_SECRET

Deploy no EKS via GitHub Actions

# .github/workflows/deploy-eks.yml
name: Deploy no EKS

on:
  push:
    branches: [main]

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

    permissions:
      contents: read
      id-token: write

    steps:
      - uses: actions/checkout@v4

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

      - name: Login no ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Constrói e publica imagem
        id: build
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/minha-api:$IMAGE_TAG .
          docker push $ECR_REGISTRY/minha-api:$IMAGE_TAG
          echo "image=$ECR_REGISTRY/minha-api:$IMAGE_TAG" >> $GITHUB_OUTPUT

      - name: Configura kubectl
        run: |
          aws eks update-kubeconfig \
            --name minha-api-production \
            --region us-east-1

      - name: Atualiza imagem no Deployment
        run: |
          kubectl set image deployment/minha-api \
            api=${{ steps.build.outputs.image }} \
            -n producao

      - name: Aguarda rollout completar
        run: |
          kubectl rollout status deployment/minha-api \
            -n producao \
            --timeout=300s

      - name: Verifica pods saudáveis
        run: |
          kubectl get pods -n producao -l app=minha-api

          READY=$(kubectl get deployment minha-api -n producao \
            -o jsonpath='{.status.readyReplicas}')
          DESIRED=$(kubectl get deployment minha-api -n producao \
            -o jsonpath='{.spec.replicas}')

          if [ "$READY" != "$DESIRED" ]; then
            echo "ERRO: $READY/$DESIRED pods prontos"
            kubectl describe deployment minha-api -n producao
            exit 1
          fi

          echo "Deploy concluído: $READY/$DESIRED pods saudáveis"

StorageClass com EBS CSI Driver

Para workloads stateful — bancos de dados, filas, caches — o EBS CSI Driver provê volumes persistentes via EBS:

# kubernetes/storage/storage-class.yml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: gp3
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
parameters:
  type: gp3
  iops: "3000"
  throughput: "125"
  encrypted: "true"
  kmsKeyId: arn:aws:kms:us-east-1:123456789:key/abc-def

O Que Vem a Seguir

O próximo artigo encerra o tema abordando as práticas avançadas de Kubernetes em produção — segurança com Pod Security Standards, GitOps com ArgoCD para deploys declarativos, e estratégias de deploy avançadas como Blue-Green e Canary no contexto do Kubernetes.


Referências para Aprofundamento

Documentação oficial - Amazon EKS Documentation — docs.aws.amazon.com — Documentação completa do EKS, incluindo guias de provisionamento, add-ons gerenciados, IRSA e boas práticas de segurança. - Karpenter Documentation — karpenter.sh — Documentação oficial do Karpenter, incluindo conceitos de NodePool, EC2NodeClass e estratégias de consolidação.

Módulos Terraform - terraform-aws-modules/eks — GitHub — Módulo oficial para provisionamento de clusters EKS, com exemplos completos de configuração para diferentes casos de uso. - terraform-aws-modules/iam — GitHub — Módulo para criação de IAM roles com IRSA, simplificando significativamente a configuração de permissões para workloads EKS.

Ferramentas do ecossistema - External Secrets Operator — external-secrets.io — Documentação do External Secrets Operator com guias de integração para AWS Secrets Manager, HashiCorp Vault e outros provedores. - AWS Load Balancer Controller — kubernetes-sigs.github.io — Documentação do AWS Load Balancer Controller com referência completa de annotations para configuração de ALBs via Ingress.

Comentários

Mais em DevOps

Platform Engineering: Construindo a Plataforma Interna de Desenvolvimento
Platform Engineering: Construindo a Plataforma Interna de Desenvolvimento

Quando uma organização tem dois ou três times de desenvolvimento, o modelo De...

GitLab Self-Hosted: Soberania Total sobre Código e Pipelines
GitLab Self-Hosted: Soberania Total sobre Código e Pipelines

Há contextos em que usar um serviço SaaS para hospedar código e pipelines não...

Azure Kubernetes Service (AKS)
Azure Kubernetes Service (AKS)

O Kubernetes em si é a mesma plataforma, seja no EKS da AWS ou no AKS do Azur...