Cluster EKS com Terraform do Zero: VPC, Control Plane e Node Group

Subir um cluster EKS pelo console leva poucos cliques, mas deixa para trás uma VPC sem padrão, IAM roles com nomes aleatórios e nenhum registro do que foi criado. Com Terraform, o mesmo cluster vira código revisável, que você recria igual em outra conta ou região e destrói sem sobras.

Este é o primeiro artigo da série artigos-eks. Aqui montamos a base que os próximos artigos vão reaproveitar: VPC com subnets públicas e privadas, control plane do Amazon EKS com autenticação por Access Entries, um managed node group e os add-ons essenciais. Tudo com módulos reutilizáveis, um por responsabilidade.

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.

Sumário

Pré-requisitos

Para criar o cluster EKS você vai precisar do AWS CLI v2 autenticado, do Terraform >= 1.5, do kubectl e de uma identidade IAM com permissão para criar VPC, IAM roles e recursos do EKS. O provider usado é o hashicorp/aws ~> 6.0.

aws sts get-caller-identity --query Arn --output text
terraform version
kubectl version --client

Antes de escolher a versão do Kubernetes, consulte quais o EKS suporta e até quando cada uma fica em suporte padrão:

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

Neste artigo o cluster nasce na 1.34. A versão fica numa variável, então trocar depois é só mudar o valor (o upgrade sem downtime tem um artigo próprio mais adiante na série).

Se o seu Terraform for antigo e o provider reclamar da versão do core, o post Unsupported terraform core version aws mostra como atualizar.

Arquitetura do cluster EKS

O desenho é o mínimo recomendado para um cluster EKS que vai receber aplicações de verdade:

  • VPC em 2 Availability Zones, com uma subnet pública e uma privada em cada uma. O EKS exige subnets em pelo menos duas AZs.
  • Nós nas subnets privadas, saindo para a internet pelo NAT Gateway (para baixar imagens, por exemplo).
  • Subnets públicas reservadas para load balancers internet-facing.
  • Control plane gerenciado pela AWS, com endpoint privado sempre ligado e endpoint público restrito ao IP da sua rede.

Cada peça é um módulo separado no repositório: terraform-vpc-modules, terraform-eks-modules, terraform-eks-node-group-modules e terraform-eks-addon-modules. O arquivo do ambiente só compõe os módulos.

VPC: subnets públicas, privadas e NAT Gateway

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

  name               = "vpc-${var.cluster_name}"
  cidr_block         = var.vpc_cidr
  az_count           = 2
  single_nat_gateway = true
}

Dentro do módulo, cada subnet já sai com a tag que o EKS usa para descobrir onde criar load balancers. Sem essas tags, um Service do tipo LoadBalancer ou um Ingress com ALB não acha subnet nenhuma:

resource "aws_subnet" "public" {
  count = var.az_count

  vpc_id                  = aws_vpc.this.id
  availability_zone       = local.azs[count.index]
  cidr_block              = cidrsubnet(var.cidr_block, var.subnet_newbits, count.index)
  map_public_ip_on_launch = true

  tags = merge(var.tags, var.public_subnet_tags, {
    Name                     = "${var.name}-public-${local.azs[count.index]}"
    "kubernetes.io/role/elb" = "1"
  })
}

resource "aws_subnet" "private" {
  count = var.az_count

  vpc_id            = aws_vpc.this.id
  availability_zone = local.azs[count.index]
  cidr_block        = cidrsubnet(var.cidr_block, var.subnet_newbits, count.index + var.az_count)

  tags = merge(var.tags, var.private_subnet_tags, {
    Name                              = "${var.name}-private-${local.azs[count.index]}"
    "kubernetes.io/role/internal-elb" = "1"
  })
}

Com 10.0.0.0/16 e subnet_newbits = 4, o plan mostra as quatro subnets em /20: 10.0.0.0/20 e 10.0.16.0/20 públicas, 10.0.32.0/20 e 10.0.48.0/20 privadas. São 4.096 endereços por subnet privada. Isso importa no EKS porque, com o VPC CNI padrão, cada pod recebe um IP da subnet; subnet pequena acaba antes da CPU dos nós.

single_nat_gateway = true economiza num lab, mas é um ponto único de falha: se a AZ do NAT cair, os nós da outra AZ ficam sem saída. Em produção, use false para ter um NAT Gateway por AZ.

Control plane do cluster EKS

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

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

  public_access_cidrs = var.public_access_cidrs
}

O coração do módulo é o aws_eks_cluster. Três blocos merecem atenção:

resource "aws_eks_cluster" "this" {
  name     = var.name
  version  = var.kubernetes_version
  role_arn = aws_iam_role.cluster.arn

  access_config {
    authentication_mode                         = var.authentication_mode
    bootstrap_cluster_creator_admin_permissions = var.bootstrap_cluster_creator_admin_permissions
  }

  vpc_config {
    subnet_ids              = var.subnet_ids
    endpoint_private_access = true
    endpoint_public_access  = var.endpoint_public_access
    public_access_cidrs     = var.public_access_cidrs
  }

  upgrade_policy {
    support_type = var.support_type
  }

  enabled_cluster_log_types = var.enabled_cluster_log_types

  tags = var.tags

  depends_on = [aws_iam_role_policy_attachment.cluster]
}
  • access_config: o modo API controla o acesso ao cluster por Access Entries, recursos da API do EKS, em vez do antigo ConfigMap aws-auth. O bootstrap_cluster_creator_admin_permissions = true dá admin a quem rodou o apply, para você não ficar trancado fora no primeiro acesso.
  • vpc_config: endpoint privado sempre ligado (os nós falam com a API por dentro da VPC) e endpoint público filtrado por public_access_cidrs. Deixar 0.0.0.0/0 expõe a API do Kubernetes para a internet inteira, protegida só pela autenticação.
  • upgrade_policy: com STANDARD, quando a versão sai do suporte padrão o EKS atualiza o control plane sozinho. Com EXTENDED, o cluster fica mais 12 meses na versão, mas a hora do control plane passa a custar bem mais.

A variável public_access_cidrs é obrigatória de propósito: o ambiente não aceita rodar sem você informar o IP que pode chamar a API. Descubra o seu com curl -s https://checkip.amazonaws.com e use /32.

Managed node group

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"]

  min_size     = 1
  desired_size = 2
  max_size     = 3
}

No managed node group, a AWS cuida do Auto Scaling Group, da AMI otimizada e da troca dos nós em upgrades. O módulo cria uma IAM role para os nós com quatro políticas: AmazonEKSWorkerNodePolicy, AmazonEKS_CNI_Policy, AmazonEC2ContainerRegistryPullOnly (só leitura de imagens no ECR) e AmazonSSMManagedInstanceCore, que permite entrar no nó pelo Session Manager sem chave SSH e sem porta 22 aberta.

A AMI padrão do módulo é a AL2023_x86_64_STANDARD (Amazon Linux 2023). O Amazon Linux 2 está em fim de vida, e o EKS deixou de publicar AMIs AL2 otimizadas a partir do Kubernetes 1.33.

Um detalhe que evita dor de cabeça: o módulo ignora mudanças em desired_size depois da criação. Quando um autoscaler (ou você, pelo console) muda a quantidade de nós, o próximo terraform plan não tenta voltar ao número do código.

Se você já criou node groups pelo console, compare com o passo a passo do post Create Node Groups EKS AWS: são os mesmos campos, só que agora versionados.

Add-ons essenciais

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

  cluster_name       = module.eks.name
  kubernetes_version = module.eks.kubernetes_version

  addons = {
    vpc-cni                = {}
    coredns                = {}
    kube-proxy             = {}
    eks-pod-identity-agent = {}
  }

  depends_on = [module.ng_default]
}

Instalar vpc-cni, coredns e kube-proxy como add-ons gerenciados faz o EKS cuidar das versões compatíveis com o cluster. Sem versão informada, o módulo consulta a mais recente compatível com aws_eks_addon_version. O eks-pod-identity-agent já entra agora porque a série usa EKS Pod Identity para dar permissões de AWS aos pods.

O depends_on no node group não está ali à toa: o CoreDNS só fica ACTIVE quando existe nó para rodar os pods dele. Sem a dependência, o Terraform pode criar o add-on antes dos nós e esperar até estourar o timeout.

terraform plan

terraform init
terraform validate
terraform plan -var 'public_access_cidrs=["SEU_IP/32"]' -out=tfplan

O código deste artigo foi validado com terraform validate e terraform plan contra uma conta AWS real. O plan fecha em 28 recursos:

  # module.vpc.aws_vpc.this will be created
  # module.vpc.aws_subnet.private[0] will be created
  # module.vpc.aws_nat_gateway.this[0] will be created
  # module.eks.aws_eks_cluster.this will be created
  # module.ng_default.aws_eks_node_group.this will be created
  # module.addons.aws_eks_addon.this["coredns"] will be created
  ...
Plan: 28 to add, 0 to change, 0 to destroy.

Ao aplicar (terraform apply tfplan), conte com algo entre 10 e 15 minutos: o control plane é a etapa mais demorada, e o node group só começa depois dele.

Outputs do Terraform

O ambiente expõe oito outputs. Eles servem para encaixar o cluster EKS em outras automações (pipeline, outro state, scripts) sem precisar abrir o console. No plan, só os valores que vêm das variáveis já aparecem; o resto fica como (known after apply) até o 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_endpoint   = (known after apply)
  + cluster_name       = "eks-blog-castilho"
  + kubeconfig_command = "aws eks update-kubeconfig --region us-east-1 --name eks-blog-castilho"
  + kubernetes_version = "1.34"
  + node_role_arn      = (known after apply)
  + private_subnet_ids = [
      + (known after apply),
      + (known after apply),
    ]
  + vpc_id             = (known after apply)
  • cluster_name e kubernetes_version: nome e versão do control plane, úteis em pipelines que chamam o AWS CLI ou checam a versão antes de um deploy.
  • cluster_endpoint: URL da API do Kubernetes, para configurar os providers kubernetes e helm em outro state.
  • vpc_id e private_subnet_ids: rede do cluster, para criar banco de dados, cache ou VPC endpoints na mesma VPC.
  • node_role_arn: IAM role dos nós, quando outro recurso precisa confiar nela (uma bucket policy, por exemplo).
  • addon_versions: versão que o EKS escolheu para cada add-on, o registro que você vai querer antes de um upgrade.
  • kubeconfig_command: o comando pronto para configurar o kubectl.

Depois do apply, leia um output com terraform output. O -raw tira as aspas, e o -json é o formato para scripts:

terraform output                       # todos
terraform output -raw cluster_endpoint
terraform output -json private_subnet_ids | jq -r '.[]'

Acessando o cluster com kubectl

O output kubeconfig_command já traz o comando:

terraform output -raw kubeconfig_command
# aws eks update-kubeconfig --region us-east-1 --name eks-blog-castilho

aws eks update-kubeconfig --region us-east-1 --name eks-blog-castilho
kubectl get nodes -o wide
kubectl get pods -n kube-system

Como a identidade que rodou o apply ganhou admin pelo bootstrap_cluster_creator_admin_permissions, o kubectl funciona de primeira. Para dar acesso a outras pessoas e times, com permissão por namespace, a série tem um artigo dedicado às Access Entries.

Custos e limpeza

Antes de aplicar, saiba o que entra na conta (valores de referência em us-east-1, confira na página de preços):

  • Control plane do EKS: US$ 0,10 por hora em suporte padrão, cerca de US$ 73 por mês, com ou sem pods rodando.
  • NAT Gateway: cobrança por hora e por GB processado. É comum ser o segundo maior item da fatura de um lab.
  • Nós EC2: dois t3.medium sob demanda, mais os volumes EBS.

Para um lab, destrua tudo ao terminar. O destroy remove os add-ons, o node group, o cluster e a VPC na ordem certa:

terraform destroy

Se você tiver criado um Service do tipo LoadBalancer pelo kubectl, apague-o antes do destroy. O load balancer criado pelo Kubernetes não está no state do Terraform e prende as subnets, travando a exclusão da VPC.

Conclusão

Com quatro módulos e um arquivo de ambiente curto, o cluster EKS sai com as decisões que costumam ficar para depois já tomadas: nós em subnet privada, subnets com as tags dos load balancers, endpoint da API restrito, acesso por Access Entries e add-ons gerenciados pelo próprio EKS.

Os próximos artigos da série partem deste mesmo cluster e acrescentam uma peça por vez: node groups com instâncias Spot, controle de acesso por time, permissões de AWS para os pods, armazenamento, Ingress, autoscaling com Karpenter, observabilidade e, por fim, o upgrade de versão sem downtime.

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