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.
Exercícios
Exercício 1
O Deployment que usa IRSA declara serviceAccountName: minha-api e nenhuma credencial AWS. Um desenvolvedor cria um novo Deployment no namespace producao, copia o container mas esquece a linha serviceAccountName. O pod sobe normalmente e as chamadas ao S3 funcionam em staging, mas falham em produção com AccessDenied. Explique o que está acontecendo — e por que "funcionar" seria pior do que falhar.
Ver resposta
✓ Resposta: Sem serviceAccountName, o pod usa a ServiceAccount default do namespace — que não tem a annotation eks.amazonaws.com/role-arn. Sem essa annotation, o webhook do EKS não injeta o token OIDC nem as variáveis AWS_ROLE_ARN/AWS_WEB_IDENTITY_TOKEN_FILE, e a cadeia de credenciais do SDK não encontra o IRSA. Ela então segue para o próximo elo: o IMDS do nó, assumindo o instance profile do node group. O AccessDenied vem daí — a role do nó não tem permissão no bucket.
E é justamente por isso que funcionar seria pior. Se a role do node group tivesse a permissão (algo comum em clusters que herdaram políticas amplas), o pod acessaria o S3 normalmente e ninguém notaria. Só que a partir daí todo pod naquele nó teria as mesmas permissões, independentemente da service account — o isolamento por workload que o IRSA existe para dar simplesmente não estaria valendo. O erro só apareceria muito depois, num incidente de segurança.
A diferença entre os ambientes é o sintoma clássico dessa confusão: staging "funciona" porque a role dos nós é mais permissiva ali. Duas defesas contra o problema: restringir a role do node group ao mínimo (CNI, ECR pull, logs) e bloquear o acesso dos pods ao IMDS — com httpPutResponseHopLimit = 1 nas instâncias, o metadata service deixa de ser alcançável de dentro do container.
Exercício 2
O node group gpu tem min_size = 0 e desired_size = 0 — nenhum nó rodando — e um taint nvidia.com/gpu=true:NO_SCHEDULE. Se o taint impede o agendamento e não há nós, para que serve esse node group? O que um pod de inferência precisa declarar para de fato rodar nele?
Ver resposta
✓ Resposta: O node group serve como capacidade sob demanda a custo zero quando ociosa. Instâncias g4dn.xlarge são caras para ficar ligadas sem uso; com desired_size = 0, nada é cobrado até que um pod exija GPU. Aí o autoscaler sobe um nó (até max_size = 5) e o derruba quando a carga acaba. O capacity_type = "SPOT" reduz mais ainda o custo, aceitando em troca interrupções com dois minutos de aviso — tolerável para inferência em lote, arriscado para requisição síncrona.
O taint não impede o uso: ele reserva esses nós. Sem ele, assim que um nó GPU subisse, qualquer pod comum sem destino poderia ser agendado nele — ocupando uma máquina cara sem tocar na GPU. O taint garante que só entra quem declarar explicitamente que quer.
O pod precisa de três coisas — as duas primeiras são frequentemente confundidas:
spec:
nodeSelector: # 1. atrai: escolhe o nó com esse label
role: gpu
tolerations: # 2. permite: aceita o taint do nó
- key: nvidia.com/gpu
value: "true"
effect: NoSchedule
containers:
- name: inferencia
resources:
limits:
nvidia.com/gpu: 1 # 3. reserva a GPU de fato
A toleration sozinha apenas permite o agendamento — não direciona nada; sem o nodeSelector o pod pode acabar em um nó comum. E o limits: nvidia.com/gpu: 1 é o que reserva o dispositivo: sem ele o container sobe no nó GPU mas não enxerga a placa. Esse recurso só existe se o NVIDIA device plugin estiver instalado no cluster, expondo a GPU como recurso agendável.
Exercício 3
A StorageClass gp3 usa volumeBindingMode: WaitForFirstConsumer. O modo alternativo, Immediate, cria o volume assim que o PVC é criado — o que parece mais rápido e mais simples. Que falha o WaitForFirstConsumer evita em um cluster com nós espalhados por várias AZs?
Ver resposta
✓ Resposta: Evita o pod ficar permanentemente Pending por causa de uma incompatibilidade de zona. Volumes EBS são zonais: um volume criado em us-east-1a só pode ser montado por uma instância em us-east-1a.
Com Immediate, o EBS CSI Driver provisiona o volume no instante em que o PVC aparece — antes de existir qualquer pod, e portanto sem saber onde ele será agendado. A zona é escolhida sem essa informação. Quando o scheduler finalmente for posicionar o pod, ele fica restrito à AZ do volume; se ali não houver nó com recursos livres (ou se o Karpenter tiver consolidado os nós daquela zona), o pod não é agendado — e não adianta haver capacidade sobrando nas outras AZs. O volume "prende" o pod a uma zona escolhida às cegas.
Com WaitForFirstConsumer, a ordem se inverte: o PVC fica em Pending até que um pod o consuma; o scheduler escolhe o nó considerando recursos, afinidades e taints; só então o volume é criado, já na zona certa. A decisão de agendamento passa a mandar no armazenamento, e não o contrário.
Vale notar o efeito colateral, que costuma assustar quem vê pela primeira vez: um PVC criado sem nenhum pod associado fica indefinidamente em Pending — e isso é o comportamento esperado, não um erro. O allowVolumeExpansion: true resolve outro problema, independente: permite aumentar o volume depois, editando o PVC, sem recriar nada.
Exercício 4
O NodePool do Karpenter define consolidationPolicy: WhenUnderutilized com consolidateAfter: 30s, e expireAfter: 720h. Um time relata que, em horários de baixa demanda, seus pods são reiniciados várias vezes sem que ninguém tenha feito deploy. Explique a causa e diga o que falta configurar.
Ver resposta
✓ Resposta: É a consolidação do Karpenter funcionando exatamente como configurada. Com WhenUnderutilized, ele procura continuamente por nós subutilizados cujos pods caibam em outros nós já existentes (ou em um nó menor e mais barato). Ao encontrar, faz o cordon, despeja os pods e termina a instância. Em baixa demanda é precisamente quando há mais nós ociosos — daí a correlação com o horário. O consolidateAfter: 30s torna o gatilho bem agressivo: meio minuto de subutilização já basta.
O expireAfter: 720h soma um segundo motivo de reciclagem: todo nó é substituído após 30 dias, para que o cluster não acumule instâncias com AMIs antigas e sem patches. É desejável do ponto de vista de segurança, e igualmente invisível para quem só observa os pods reiniciando.
O que falta é um PodDisruptionBudget. Ele é o contrato que limita quantos pods de um workload podem ficar indisponíveis simultaneamente em despejos voluntários — e a consolidação do Karpenter respeita PDBs:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: minha-api
namespace: producao
spec:
minAvailable: 2 # ou maxUnavailable: 1
selector:
matchLabels:
app: minha-api
Com isso o Karpenter continua consolidando, mas de forma escalonada, sem nunca derrubar réplicas demais ao mesmo tempo. Duas ressalvas: um PDB mal calibrado (minAvailable igual ao número de réplicas) bloqueia a consolidação por completo e trava também os upgrades do cluster; e um workload que realmente não pode ser movido deve usar a annotation karpenter.sh/do-not-disrupt: "true" no pod — que exclui aquele nó da consolidação enquanto o pod existir.
Exercício 5
No workflow de deploy, o passo kubectl rollout status --timeout=300s falha porque a imagem nova tem um bug e nenhum pod fica ready. O job do GitHub Actions termina em vermelho. Descreva em que estado o cluster fica nesse momento — e se os usuários estão sendo afetados.
Ver resposta
✓ Resposta: Os usuários não são afetados, e nada é revertido — o cluster fica travado no meio do rollout.
Como o Deployment usa maxUnavailable: 0, o controlador só remove um pod antigo depois que um pod novo passa no readinessProbe. Como nenhum passa, nenhum pod antigo é removido: as três réplicas da versão anterior continuam no ar servindo tráfego normalmente. Ao lado delas fica o pod novo permitido pelo maxSurge: 1, em CrashLoopBackOff ou nunca ficando ready — e fora do Service, porque o readiness reprova. É o rollout progressivo protegendo a produção: a versão quebrada nunca recebe uma requisição real.
O ponto importante é que o --timeout apenas encerra a espera do comando; ele não cancela nem reverte coisa alguma. O spec do Deployment continua apontando para a imagem defeituosa, e o controlador segue tentando indefinidamente. O job vermelho é só o mensageiro. Enquanto ninguém intervier, o cluster permanece assim — o que é seguro para os usuários, mas bloqueia todos os deploys seguintes, inclusive o do commit que corrige o bug.
Para reverter é preciso um passo explícito, que o workflow não tem:
- name: Rollback em caso de falha
if: failure()
run: |
kubectl rollout undo deployment/minha-api -n producao
kubectl rollout status deployment/minha-api -n producao --timeout=120s
Um detalhe que sustenta esse rollback: o Deployment do artigo anterior traz a annotation kubernetes.io/change-cause, que é o que dá conteúdo útil ao kubectl rollout history — sem ela, o histórico lista revisões sem dizer o que cada uma mudou.