Upgrade do EKS sem Downtime com Terraform: Control Plane, Add-ons e Node Groups

O upgrade do EKS não é opcional. Cada versão do Kubernetes tem cerca de 14 meses de suporte padrão no EKS. Depois disso, ou o cluster sobe de versão sozinho, ou você paga bem mais caro pelo suporte estendido. E não existe volta: o control plane não faz downgrade. Por isso vale ter um roteiro testado, em etapas, que não derrube as aplicações.

Este é o nono artigo da série artigos-eks, que começou com o cluster EKS com Terraform do zero. Aqui o mesmo cluster sobe de 1.35 para 1.36 em duas etapas: primeiro o control plane e os add-ons, depois os nós.

Segurança: os comandos deste guia usam o perfil do AWS CLI para autenticar. Nunca coloque access keys no código ou em .tfvars versionado; prefira aws sso login ou um perfil com credenciais temporárias. Faça o upgrade primeiro num cluster de homologação com as mesmas aplicações.

Sumário

Pré-requisitos

Você vai precisar do AWS CLI v2 autenticado, do Terraform >= 1.5, do kubectl na mesma versão menor do destino (ou uma acima) e de permissão para atualizar o cluster e os node groups. O provider usado é o hashicorp/aws ~> 6.0.

O ambiente deste artigo recria a base do primeiro artigo, agora na 1.35 e com versões separadas para o control plane e para o node group. Se você usa o cluster da série, a mudança é só nas variáveis.

Suporte padrão, estendido e o custo de adiar o upgrade do EKS

O calendário de cada versão sai direto da API do EKS:

aws eks describe-cluster-versions \
  --query 'clusterVersions[].[clusterVersion,versionStatus,endOfStandardSupportDate]' \
  --output table

Em setembro de 2026, a 1.36 é a versão mais nova (disponível desde 1º de junho) e a 1.35 fica em suporte padrão até março de 2027. Quando o suporte padrão acaba, o que acontece depende do upgrade_policy do cluster:

  • STANDARD: o EKS atualiza o control plane sozinho para a próxima versão. O horário não é seu, e as aplicações precisam estar prontas.
  • EXTENDED: o cluster ganha mais 12 meses na versão. Em compensação, a hora do control plane sai de US$ 0,10 para US$ 0,60, cerca de US$ 438 por mês por cluster em vez de US$ 73.

O suporte estendido é uma rede de segurança, não um plano. Com dez clusters, adiar um ano custa mais de US$ 40 mil só de control plane. Fazer o upgrade do EKS a cada versão, com roteiro, sai mais barato e dá menos susto.

A ordem do upgrade do EKS

  1. Checar os upgrade insights e corrigir o que eles apontarem.
  2. Subir o control plane, uma versão menor por vez. O EKS não pula versão: de 1.34 para 1.36 são dois upgrades.
  3. Atualizar os add-ons para versões compatíveis com o novo control plane.
  4. Trocar os nós, respeitando os PodDisruptionBudgets.

Os nós vêm por último porque o Kubernetes aceita kubelet mais antigo que o control plane (até três versões menores atrás), mas não o contrário. Um node group nunca pode estar numa versão acima do cluster.

Antes de começar: upgrade insights

O EKS analisa o cluster e aponta o que pode quebrar na próxima versão: APIs removidas que algum cliente ainda chama, diferença de versão entre kubelet e control plane, add-ons incompatíveis.

aws eks list-insights \
  --cluster-name eks-blog-castilho \
  --filter categories=UPGRADE_READINESS \
  --query 'insights[].[name,insightStatus.status]' \
  --output table

aws eks describe-insight \
  --cluster-name eks-blog-castilho \
  --id <id-do-insight>

Só siga com todos os insights em PASSING. Um ERROR de API removida, por exemplo, quer dizer que algum manifesto ou Helm chart ainda usa uma versão de API que deixa de existir. Depois do upgrade, esse recurso para de ser aceito pelo cluster.

PodDisruptionBudget: o que garante o zero downtime

Na etapa dos nós, o EKS esvazia os nós antigos com drain. Sem PodDisruptionBudget, nada impede que todas as réplicas de uma aplicação sejam removidas ao mesmo tempo. Com ele, o drain respeita o mínimo de réplicas disponíveis:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: loja-web
  namespace: loja
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: loja-web

Dois cuidados. Primeiro, o PDB só protege quem tem mais de uma réplica: com uma réplica e maxUnavailable: 0, o drain nunca termina. Segundo, se um PDB segurar a remoção de um pod por 15 minutos, o update do managed node group falha. Revise os PDBs antes de começar:

kubectl get pdb -A

Na coluna ALLOWED DISRUPTIONS, valor zero é sinal de que o drain vai travar nesse PDB.

Versões separadas no Terraform

O truque do ambiente é ter uma variável para o control plane e outra para os nós. Assim cada etapa é um apply diferente:

module "eks" {
  source = "../../terraform-eks-modules"

  name               = var.cluster_name
  kubernetes_version = var.cluster_version
  subnet_ids         = module.vpc.private_subnet_ids
  support_type       = var.support_type

  public_access_cidrs = var.public_access_cidrs
}

module "ng_default" {
  source = "../../terraform-eks-node-group-modules"

  cluster_name       = module.eks.name
  name               = "default"
  subnet_ids         = module.vpc.private_subnet_ids
  instance_types     = ["t3.medium"]
  kubernetes_version = var.node_group_version

  min_size     = 2
  desired_size = 3
  max_size     = 4

  max_unavailable_percentage = 33
}

O node group fica com três nós e max_unavailable_percentage = 33: um nó por vez sai de cena durante a troca. Com um nó só, não existe upgrade sem interrupção, porque não há para onde mover os pods.

Os add-ons recebem a versão do cluster (module.eks.kubernetes_version) e, sem versão fixa, o módulo busca a mais recente compatível. Quando o control plane muda, a versão dos add-ons acompanha no mesmo apply. Os detalhes desse módulo estão no artigo sobre add-ons do EKS.

Etapa 1: control plane e add-ons

terraform plan \
  -var 'public_access_cidrs=["SEU_IP/32"]' \
  -var cluster_version=1.36 \
  -out=tfplan
terraform apply tfplan

Num cluster existente, o plan dessa etapa mostra uma atualização no lugar do aws_eks_cluster, sem recriar nada. O trecho abaixo é ilustrativo, no formato que o Terraform usa para atualização in-place. Ele não foi gerado neste artigo, porque o código não foi aplicado e não existe state:

# Exemplo ilustrativo (formato de update in-place, não executado)
  # module.eks.aws_eks_cluster.this will be updated in-place
  ~ resource "aws_eks_cluster" "this" {
      ~ version = "1.35" -> "1.36"
        # (demais atributos sem mudança)
    }

O que nunca pode aparecer nesse plan é must be replaced no cluster: isso significaria apagar e recriar o control plane. Se aparecer, pare e descubra qual atributo provocou a troca.

O update do control plane leva dezenas de minutos e as aplicações continuam rodando, porque os nós não mudam. Acompanhe pelo console ou pelo CLI:

aws eks describe-cluster --name eks-blog-castilho \
  --query 'cluster.[version,status]' --output text

kubectl version

Etapa 2: node group

terraform plan \
  -var 'public_access_cidrs=["SEU_IP/32"]' \
  -var cluster_version=1.36 \
  -var node_group_version=1.36 \
  -out=tfplan
terraform apply tfplan

Com a nova versão, o managed node group sobe nós novos com a AMI da 1.36, isola os antigos (cordon), remove os pods com drain respeitando os PDBs e só então desliga os nós antigos. Em outro terminal, dá para ver a troca acontecendo:

kubectl get nodes -o custom-columns=NOME:.metadata.name,VERSAO:.status.nodeInfo.kubeletVersion -w

aws eks list-updates --name eks-blog-castilho --nodegroup-name default

Se tiver node groups com Spot, como os do artigo sobre managed node groups, faça um node group por vez. Se o cluster usa Karpenter, os nós dele seguem outro caminho: o Karpenter troca os nós quando a AMI configurada muda.

terraform plan

O código deste artigo foi validado com terraform validate e terraform plan contra uma conta AWS real, sem apply e, portanto, sem state. Foram dois plans de criação, um em cada versão, os dois com 28 recursos:

terraform init
terraform validate

# Versão de partida (padrão das variáveis: 1.35)
terraform plan -var 'public_access_cidrs=["SEU_IP/32"]' -out=tfplan
Plan: 28 to add, 0 to change, 0 to destroy.

# Versão alvo
terraform plan -var 'public_access_cidrs=["SEU_IP/32"]' \
  -var cluster_version=1.36 -var node_group_version=1.36 -out=tfplan
  # module.eks.aws_eks_cluster.this will be created
      + version                       = "1.36"
  # module.ng_default.aws_eks_node_group.this will be created
      + version                = "1.36"
  # module.addons.aws_eks_addon.this["coredns"] will be created
  ...
Plan: 28 to add, 0 to change, 0 to destroy.

O plan na versão alvo confirma que o código aceita a 1.36 em todos os módulos, sem erro de validação. A troca de versão no lugar, com o ~ version da etapa 1, só aparece num cluster que já existe.

Outputs do Terraform

O ambiente expõe cinco outputs. O bloco abaixo é o Changes to Outputs do plan na versão alvo. Como não houve apply, as versões dos add-ons aparecem como (known after apply):

Changes to Outputs:
  + addon_versions     = {
      + coredns                = (known after apply)
      + eks-pod-identity-agent = (known after apply)
      + kube-proxy             = (known after apply)
      + vpc-cni                = (known after apply)
    }
  + cluster_name       = "eks-blog-castilho"
  + cluster_version    = "1.36"
  + kubeconfig_command = "aws eks update-kubeconfig --region us-east-1 --name eks-blog-castilho"
  + node_group_name    = "default"
  • cluster_name: nome do cluster, para os comandos de list-insights e describe-cluster (--cluster-name $(terraform output -raw cluster_name)).
  • cluster_version: versão do control plane depois do apply. É o jeito rápido de confirmar que a etapa 1 terminou: terraform output -raw cluster_version.
  • node_group_name: nome do node group, para acompanhar a etapa 2 com aws eks list-updates --name ... --nodegroup-name $(terraform output -raw node_group_name).
  • addon_versions: versão de cada add-on. Salve antes e depois do upgrade (terraform output -json addon_versions > addons-antes.json) para ter o registro do que mudou.
  • kubeconfig_command: comando pronto para configurar o kubectl. Rode com $(terraform output -raw kubeconfig_command).

Custos e limpeza

O upgrade em si não tem cobrança. Durante a etapa 2, por alguns minutos, existem nós novos e antigos ao mesmo tempo, e você paga pelos dois. O custo que importa é o de não fazer o upgrade: o suporte estendido multiplica por seis a hora do control plane.

O ambiente deste artigo usa três nós t3.medium, um a mais que o primeiro artigo, para permitir a troca sem interrupção. Some o control plane e o NAT Gateway, detalhados no primeiro artigo da série. Para limpar o lab:

terraform destroy -var 'public_access_cidrs=["SEU_IP/32"]'

Conclusão

O upgrade do EKS sem downtime depende menos do Terraform e mais da ordem: insights limpos, control plane, add-ons e, por último, os nós, com PodDisruptionBudgets que permitam o drain. No código, a única mudança necessária é separar a versão do control plane da versão dos nós.

Com isso a série artigos-eks fecha o ciclo: cluster do zero, node groups, acesso, permissões para pods, add-ons, Ingress, Karpenter, Container Insights e, agora, como manter tudo isso atualizado.

Referências oficiais

Interessado em saber mais sobre esse assunto?

Deixe uma resposta

Descubra mais sobre Blog do Castilho - Tecnologia | FinOps | DevOps | Cloud

Assine agora mesmo para continuar lendo e ter acesso ao arquivo completo.

Continuar lendo