DevOps desde Cero: EKS, Corriendo Kubernetes en AWS

2026-05-27 | Gabriel Garrido | 20 min de lectura
Share:

Apoya este blog

Si te resulta util este contenido, considera apoyar el blog.

Introduccion

Bienvenido al articulo trece de la serie DevOps desde Cero. En el articulo anterior empaquetamos nuestra API TypeScript como un Helm chart. Ahora es momento de darle a ese chart un hogar real en AWS provisionando un cluster EKS.


Amazon Elastic Kubernetes Service (EKS) es la oferta de Kubernetes gestionado de AWS. Obtenes un control plane de nivel produccion que AWS parchea, escala y mantiene altamente disponible. Vos solo te preocupas por tus workloads y los worker nodes que los corren. Si veniste siguiendo la serie, ya sabes como funciona ECS del articulo ocho. EKS toma un enfoque diferente: en vez de una API propietaria, obtenes Kubernetes estandar, lo que significa que todo lo que aprendiste en los articulos once y doce (fundamentos de Kubernetes y Helm) se aplica directamente.


Si queres ver como se hacia Kubernetes en AWS antes de que EKS se convirtiera en el default, mira From zero to hero with kops and AWS. Ese articulo cubre kops, una herramienta que provisiona clusters autogestionados. EKS se convirtio desde entonces en la opcion principal para la mayoria de los equipos porque te saca de encima la carga de gestionar el control plane vos mismo.


En este articulo vamos a cubrir que es EKS, compararlo con ECS, provisionar un cluster completo con Terraform, explorar las opciones de node groups, configurar IAM Roles for Service Accounts, configurar Karpenter para autoscaling, instalar el AWS Load Balancer Controller, deployear nuestra API TypeScript, y discutir storage y consideraciones de costo. Vamos a meterle.


Que es EKS?

EKS te da un control plane de Kubernetes gestionado. Eso significa que AWS corre el API server, etcd, el scheduler, y el controller manager por vos. Estos componentes corren en multiples availability zones para alta disponibilidad, y AWS se encarga de upgrades, parches y backups.


Tus responsabilidades son:


  • Worker nodes: Vos provisionas las instancias EC2 (o Fargate profiles) donde corren tus pods. AWS ofrece managed node groups que automatizan el ciclo de vida de estas instancias, pero vos seguis decidiendo tipos de instancia, tamanos y escalado.
  • Networking: EKS se integra con tu VPC. Los pods obtienen direcciones IP de las subnets de tu VPC usando el plugin VPC CNI, lo que significa que son ciudadanos de primera clase en la red.
  • Add-ons: Cosas como CoreDNS, kube-proxy, y el VPC CNI se instalan por defecto, pero vos gestionas sus versiones y configuracion.
  • Workloads: Todo lo que deployeas, desde Deployments hasta StatefulSets y CronJobs, es tu responsabilidad.

El control plane de EKS cuesta $0.10 por hora (aproximadamente $73 por mes). Ademas de eso pagas por el compute que uses para worker nodes. Esto es importante tenerlo en cuenta cuando discutamos costos mas adelante.


EKS vs ECS: cuando usar cada uno

Tanto EKS como ECS corren containers en AWS, pero resuelven el problema de manera diferente. Asi es como pensar la eleccion:


  • EKS es Kubernetes estandar. Si tu equipo ya conoce Kubernetes, si necesitas portabilidad entre nubes, o si estas corriendo arquitecturas complejas de microservicios con operadores custom, service meshes, o scheduling avanzado, EKS es la eleccion correcta. El ecosistema es enorme, y casi todas las herramientas del landscape de CNCF funcionan out of the box.
  • ECS es nativo de AWS. Si tus workloads son sencillos, si tu equipo es chico y no quiere aprender Kubernetes, o si queres integracion estrecha con servicios de AWS sin controllers extra, ECS es mas simple y mas barato (sin costo de control plane). El tipo de lanzamiento Fargate significa que no gestionas infraestructura en absoluto.

Una regla practica: si tenes menos de cinco servicios y no tenes requerimiento de multi-cloud, empeza con ECS. Si tenes un equipo de plataforma en crecimiento, necesitas el ecosistema Kubernetes, o planeas correr en multiples proveedores, anda con EKS.


Para esta serie estamos cubriendo ambos porque los equipos reales se encuentran con los dos. Ya deployeaste a ECS en el articulo ocho. Ahora vas a ver como se compara EKS de manera practica.


Prerrequisitos

Antes de empezar, asegurate de tener lo siguiente instalado:


# AWS CLI v2
aws --version

# Terraform
terraform --version

# kubectl
kubectl version --client

# Helm
helm version

# eksctl (opcional pero util para debugging)
eksctl version

Tambien necesitas una cuenta de AWS con permisos para crear VPCs, clusters EKS, roles IAM, e instancias EC2. Si seguiste el articulo seis (AWS desde cero), ya tenes esto configurado.


Provisionando la VPC con Terraform

Los clusters EKS viven dentro de una VPC. La VPC necesita subnets publicas (para load balancers) y subnets privadas (para worker nodes). Empecemos con la base de red.


Crea un nuevo proyecto de Terraform:


mkdir -p eks-cluster/terraform
cd eks-cluster/terraform

Primero, la configuracion del provider y backend:


# providers.tf
terraform {
  required_version = ">= 1.5"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.12"
    }
    kubectl = {
      source  = "alx-v/kubectl"
      version = "~> 2.1"
    }
  }
}

provider "aws" {
  region = var.region
}

provider "helm" {
  kubernetes {
    host                   = module.eks.cluster_endpoint
    cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)

    exec {
      api_version = "client.authentication.k8s.io/v1beta1"
      command     = "aws"
      args        = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
    }
  }
}

Ahora las variables:


# variables.tf
variable "region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"
}

variable "cluster_name" {
  description = "Name of the EKS cluster"
  type        = string
  default     = "devops-zero-to-hero"
}

variable "cluster_version" {
  description = "Kubernetes version"
  type        = string
  default     = "1.31"
}

variable "vpc_cidr" {
  description = "CIDR block for the VPC"
  type        = string
  default     = "10.0.0.0/16"
}

Y la VPC usando el modulo oficial de AWS:


# vpc.tf
data "aws_availability_zones" "available" {
  filter {
    name   = "opt-in-status"
    values = ["opt-in-not-required"]
  }
}

locals {
  azs = slice(data.aws_availability_zones.available.names, 0, 3)
}

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

  name = "${var.cluster_name}-vpc"
  cidr = var.vpc_cidr

  azs             = local.azs
  private_subnets = [for k, v in local.azs : cidrsubnet(var.vpc_cidr, 4, k)]
  public_subnets  = [for k, v in local.azs : cidrsubnet(var.vpc_cidr, 8, k + 48)]
  intra_subnets   = [for k, v in local.azs : cidrsubnet(var.vpc_cidr, 8, k + 52)]

  enable_nat_gateway = true
  single_nat_gateway = true

  public_subnet_tags = {
    "kubernetes.io/role/elb" = 1
  }

  private_subnet_tags = {
    "kubernetes.io/role/internal-elb" = 1
    "karpenter.sh/discovery"         = var.cluster_name
  }

  tags = {
    Project     = "devops-zero-to-hero"
    Environment = "dev"
  }
}

Algunas cosas a notar sobre los tags de las subnets:


  • kubernetes.io/role/elb en subnets publicas le dice al AWS Load Balancer Controller donde colocar ALBs con acceso a internet.
  • kubernetes.io/role/internal-elb en subnets privadas es para load balancers internos.
  • karpenter.sh/discovery en subnets privadas permite que Karpenter encuentre subnets para lanzar nodos.

Usamos un solo NAT gateway para mantener los costos bajos en un entorno de dev. En produccion querrias uno por availability zone para redundancia.


Provisionando el cluster EKS

Ahora el evento principal. Vamos a usar el modulo oficial de EKS para Terraform, que envuelve mucha complejidad en una interfaz limpia:


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

  cluster_name    = var.cluster_name
  cluster_version = var.cluster_version

  # Acceso al cluster
  cluster_endpoint_public_access = true

  # Add-ons del cluster
  cluster_addons = {
    coredns                = {}
    eks-pod-identity-agent = {}
    kube-proxy             = {}
    vpc-cni                = {}
  }

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

  # Dar acceso admin al identity de Terraform
  enable_cluster_creator_admin_permissions = true

  # Managed node groups
  eks_managed_node_groups = {
    default = {
      instance_types = ["t3.medium"]

      min_size     = 2
      max_size     = 5
      desired_size = 2

      labels = {
        role = "general"
      }

      tags = {
        "karpenter.sh/discovery" = var.cluster_name
      }
    }
  }

  tags = {
    Project     = "devops-zero-to-hero"
    Environment = "dev"
  }
}

Esto crea un cluster EKS con un managed node group de dos instancias t3.medium. Desglosemos lo que esta pasando:


  • cluster_endpoint_public_access: Hace que la API de Kubernetes sea accesible desde internet. Para produccion podrias restringir esto a bloques CIDR especificos o usar una VPN.
  • cluster_addons: Estos son los add-ons esenciales de EKS. CoreDNS maneja service discovery, kube-proxy gestiona reglas de red, y vpc-cni les da a los pods direcciones IP nativas de la VPC.
  • enable_cluster_creator_admin_permissions: Otorga acceso admin completo al identity de IAM que crea el cluster. Sin esto, podes quedar bloqueado.
  • eks_managed_node_groups: Definimos un node group con auto-scaling entre 2 y 5 nodos.

Node groups: entendiendo tus opciones

EKS te da tres formas de correr tus workloads. Cada una tiene sus trade-offs:


  • Managed node groups: AWS maneja el ciclo de vida de las instancias EC2. Vos elegis tipos de instancia y tamanos, y AWS se encarga de provisionar, drenar, y actualizar nodos. Esta es la opcion default para la mayoria de los equipos. El ejemplo de arriba usa managed node groups.
  • Self-managed node groups: Vos creas y gestionas las instancias EC2 usando Auto Scaling Groups. Esto te da control total pero mas overhead operacional. Usa esto solo si necesitas AMIs custom, GPUs con drivers especificos, o configuraciones de instancia inusuales.
  • Fargate profiles: AWS corre tus pods en compute serverless. Nada de instancias EC2 que gestionar. Cada pod tiene su propia micro-VM aislada. Esto esta genial para batch jobs o workloads con escalado impredecible, pero tiene limitaciones: nada de DaemonSets, nada de persistent volumes respaldados por EBS, y mayor costo por pod comparado con instancias EC2 bien utilizadas.

Para la mayoria de los workloads, empeza con managed node groups. Si necesitas escalado mas sofisticado (que vamos a configurar en un rato), agrega Karpenter encima.


IAM Roles for Service Accounts (IRSA)

Este es uno de los conceptos mas importantes de EKS para entender. Tus pods muchas veces necesitan hablar con servicios de AWS: leer de S3, escribir en DynamoDB, enviar mensajes a SQS. El enfoque viejo era adjuntar politicas IAM al instance profile del nodo, pero eso significa que cada pod en ese nodo obtiene los mismos permisos. Eso es una pesadilla de seguridad.


IRSA resuelve esto permitiendote mapear un ServiceAccount de Kubernetes a un rol IAM especifico. Solo los pods que usan ese ServiceAccount obtienen esos permisos. Asi funciona internamente:


Pod (con anotacion de ServiceAccount)
  --> Kubernetes monta un token proyectado
    --> AWS STS valida el token via OIDC
      --> El pod asume el rol IAM
        --> El pod obtiene credenciales temporales de AWS

EKS crea un proveedor OpenID Connect (OIDC) para tu cluster. Cuando un pod arranca, Kubernetes inyecta un token JWT firmado. AWS STS valida este token contra el proveedor OIDC y emite credenciales temporales para el rol IAM mapeado. Sin credenciales de larga duracion, sin permisos compartidos.


Asi es como configuras IRSA para un pod que necesita acceso a S3:


# irsa.tf

# El proveedor OIDC lo crea el modulo EKS automaticamente
# Solo necesitamos crear el rol IAM y la politica

module "s3_reader_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 5.0"

  role_name = "${var.cluster_name}-s3-reader"

  role_policy_arns = {
    policy = aws_iam_policy.s3_read.arn
  }

  oidc_providers = {
    main = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["default:s3-reader"]
    }
  }
}

resource "aws_iam_policy" "s3_read" {
  name        = "${var.cluster_name}-s3-read"
  description = "Allow reading from the application S3 bucket"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:ListBucket"
        ]
        Resource = [
          "arn:aws:s3:::my-app-bucket",
          "arn:aws:s3:::my-app-bucket/*"
        ]
      }
    ]
  })
}

Despues en tu manifiesto de Kubernetes (o en los values de Helm), anotas el ServiceAccount:


apiVersion: v1
kind: ServiceAccount
metadata:
  name: s3-reader
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/devops-zero-to-hero-s3-reader

Cualquier pod que use este ServiceAccount va a recibir automaticamente credenciales temporales de AWS con el scope de ese rol IAM. Esta es la forma correcta de manejar permisos de AWS en EKS.


Cluster autoscaler vs Karpenter

Cuando tus workloads crecen, necesitas mas nodos. Hay dos opciones principales para autoscaling de nodos en EKS:


  • Cluster Autoscaler: El enfoque tradicional de Kubernetes. Observa pods que no se pueden schedulear por recursos insuficientes, y despues agrega nodos de tus node groups existentes. Funciona, pero esta limitado por las configuraciones pre-definidas de tus node groups. Si necesitas una instancia con GPU pero tu node group solo tiene t3.medium, quedaste.
  • Karpenter: El provisionador de nodos open-source de AWS. En vez de escalar node groups pre-definidos, Karpenter mira los requerimientos de los pods pendientes y provisiona el tipo de instancia correcto sobre la marcha. Puede mezclar tipos de instancia, usar instancias Spot, y dimensionar los nodos basado en las necesidades reales del workload. Es mas rapido, mas inteligente, y mas costo-efectivo.

Para clusters nuevos, Karpenter es la mejor opcion. Vamos a configurarlo.


Configurando Karpenter con Terraform

Karpenter necesita permisos IAM para lanzar instancias EC2 y gestionar su ciclo de vida. El modulo oficial de Karpenter para Terraform lo hace bastante directo:


# karpenter.tf
module "karpenter" {
  source  = "terraform-aws-modules/eks/aws//modules/karpenter"
  version = "~> 20.0"

  cluster_name = module.eks.cluster_name

  # Crear el rol IAM para el controller de Karpenter
  enable_v1_permissions = true

  # Crear el rol IAM del nodo que los nodos provisionados por Karpenter van a usar
  node_iam_role_additional_policies = {
    AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  }

  tags = {
    Project     = "devops-zero-to-hero"
    Environment = "dev"
  }
}

# Instalar Karpenter usando Helm
resource "helm_release" "karpenter" {
  namespace        = "kube-system"
  name             = "karpenter"
  repository       = "oci://public.ecr.aws/karpenter"
  chart            = "karpenter"
  version          = "1.1.1"
  wait             = false

  values = [
    <<-EOT
    serviceAccount:
      name: ${module.karpenter.service_account}
    settings:
      clusterName: ${module.eks.cluster_name}
      clusterEndpoint: ${module.eks.cluster_endpoint}
      interruptionQueue: ${module.karpenter.queue_name}
    EOT
  ]
}

Despues de instalar Karpenter, necesitas definir un NodePool y un EC2NodeClass que le digan a Karpenter que tipo de nodos provisionar:


# karpenter-nodepool.yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand", "spot"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r", "t"]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ["4"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      expireAfter: 720h
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m
  limits:
    cpu: "100"
    memory: 200Gi
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiSelectorTerms:
    - alias: al2023@latest
  role: "KarpenterNodeRole-devops-zero-to-hero"
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: devops-zero-to-hero
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: devops-zero-to-hero
  tags:
    Project: devops-zero-to-hero
    ManagedBy: karpenter

Aplica los recursos de Karpenter despues de que el cluster este listo:


kubectl apply -f karpenter-nodepool.yaml

Esto es lo que pasa en esta configuracion:


  • NodePool: Define restricciones para los nodos. Permitimos instancias tanto on-demand como spot, restringimos a familias de instancias modernas (c, m, r, t con generacion > 4), y ponemos limites de recursos para que Karpenter no levante compute ilimitado.
  • expireAfter: Los nodos se reciclan despues de 30 dias. Esto asegura que agarren las ultimas AMIs y parches de seguridad.
  • consolidationPolicy: Karpenter consolida workloads activamente. Si los nodos estan vacios o subutilizados, mueve pods y termina los nodos sobrantes para ahorrar costo.
  • EC2NodeClass: Define configuraciones especificas de AWS como la AMI, el rol IAM, y los selectores de subnets/security groups.

Con Karpenter corriendo, podes bajar tu managed node group a solo uno o dos nodos para workloads de sistema, y dejar que Karpenter maneje todo lo demas dinamicamente.


AWS Load Balancer Controller

Por defecto, los servicios de Kubernetes de tipo LoadBalancer crean Classic Load Balancers en AWS. Estos estan desactualizados. El AWS Load Balancer Controller reemplaza ese comportamiento con ALBs modernos (para HTTP/HTTPS) y NLBs (para TCP/UDP).


El controller observa recursos Ingress y anotaciones de Service, y despues crea y configura los load balancers de AWS correspondientes automaticamente. Vamos a instalarlo:


# alb-controller.tf
module "lb_controller_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 5.0"

  role_name                              = "${var.cluster_name}-lb-controller"
  attach_load_balancer_controller_policy = true

  oidc_providers = {
    main = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["kube-system:aws-load-balancer-controller"]
    }
  }
}

resource "helm_release" "aws_lb_controller" {
  namespace  = "kube-system"
  name       = "aws-load-balancer-controller"
  repository = "https://aws.github.io/eks-charts"
  chart      = "aws-load-balancer-controller"
  version    = "1.9.2"

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

  set {
    name  = "serviceAccount.name"
    value = "aws-load-balancer-controller"
  }

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

  set {
    name  = "vpcId"
    value = module.vpc.vpc_id
  }
}

Fijate como usamos IRSA aca. El Load Balancer Controller necesita permisos para crear ALBs, gestionar target groups, y leer tags de subnets. En vez de darle esos permisos al nodo, creamos un rol IAM dedicado y lo vinculamos al ServiceAccount del controller.


Una vez instalado, podes crear recursos Ingress que provisionen ALBs automaticamente:


apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: task-api
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS": 443}]'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:123456789012:certificate/abc-123
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: task-api
                port:
                  number: 3000

El controller lee las anotaciones, crea un ALB en tus subnets publicas, adjunta el certificado ACM para TLS, y rutea trafico a tus pods. Ya no necesitas gestionar load balancers manualmente.


Configurando kubeconfig

Despues de provisionar el cluster, necesitas configurar kubectl para comunicarse con el. La AWS CLI lo hace simple:


# Actualizar tu kubeconfig
aws eks update-kubeconfig --region us-east-1 --name devops-zero-to-hero

# Verificar la conexion
kubectl get nodes

Deberias ver las instancias de tu managed node group:


NAME                             STATUS   ROLES    AGE   VERSION
ip-10-0-1-42.ec2.internal       Ready    <none>   5m    v1.31.2-eks-7f9249a
ip-10-0-2-87.ec2.internal       Ready    <none>   5m    v1.31.2-eks-7f9249a

Si trabajas con multiples clusters, podes cambiar entre ellos usando contextos:


# Listar todos los contextos
kubectl config get-contexts

# Cambiar a un contexto especifico
kubectl config use-context arn:aws:eks:us-east-1:123456789012:cluster/devops-zero-to-hero

# Renombrar un contexto por conveniencia
kubectl config rename-context \
  arn:aws:eks:us-east-1:123456789012:cluster/devops-zero-to-hero \
  eks-dev

Deployeando la API TypeScript a EKS

Te acordas del Helm chart que construimos en el articulo doce? Ahora lo ponemos a trabajar. Si tenes tu chart en un registry OCI, el deploy es un solo comando:


# Crear un namespace para la aplicacion
kubectl create namespace task-api

# Instalar el chart
helm install task-api oci://ghcr.io/your-org/charts/task-api \
  --version 0.1.0 \
  --namespace task-api \
  -f values-eks.yaml

Asi se ve el archivo de values especifico para EKS:


# values-eks.yaml
replicaCount: 2

image:
  repository: 123456789012.dkr.ecr.us-east-1.amazonaws.com/task-api
  tag: "1.0.0"

service:
  type: ClusterIP
  port: 3000

ingress:
  enabled: true
  className: alb
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS": 443}]'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:123456789012:certificate/abc-123
    alb.ingress.kubernetes.io/healthcheck-path: /health
  hosts:
    - host: api.example.com
      paths:
        - path: /
          pathType: Prefix

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70

serviceAccount:
  create: true
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/task-api-role

Despues de que el deploy termine, podes verificar que todo esta corriendo:


# Chequear los pods
kubectl get pods -n task-api
NAME                        READY   STATUS    RESTARTS   AGE
task-api-6d8f9c7b4a-k2m5n   1/1     Running   0          2m
task-api-6d8f9c7b4a-x9p3r   1/1     Running   0          2m

# Chequear el ingress (el ALB tarda un minuto o dos en provisionar)
kubectl get ingress -n task-api
NAME       CLASS   HOSTS              ADDRESS                                      PORTS   AGE
task-api   alb     api.example.com    k8s-taskapi-xxxxx.us-east-1.elb.amazonaws.com   80      3m

# Testear el endpoint
curl https://api.example.com/health
{"status": "ok"}

El AWS Load Balancer Controller ve el recurso Ingress, crea un ALB, configura target groups apuntando a las IPs de tus pods, y adjunta el certificado TLS. El trafico fluye desde internet a traves del ALB directamente a tus pods.


Storage: EBS CSI driver

Si tus workloads necesitan storage persistente (bases de datos, caches, subida de archivos), necesitas el EBS CSI driver. Este driver permite que los PersistentVolumes de Kubernetes esten respaldados por volumenes EBS.


Agregalo como add-on de EKS en tu Terraform:


# Agregar a los cluster_addons en eks.tf
cluster_addons = {
  coredns                = {}
  eks-pod-identity-agent = {}
  kube-proxy             = {}
  vpc-cni                = {}
  aws-ebs-csi-driver = {
    service_account_role_arn = module.ebs_csi_irsa.iam_role_arn
  }
}

# ebs-csi.tf
module "ebs_csi_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 5.0"

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

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

Despues crea un StorageClass y usalo en tus workloads:


apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: gp3
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  fsType: ext4
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-volume
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: gp3
  resources:
    requests:
      storage: 10Gi

El modo de binding WaitForFirstConsumer es importante. Retrasa la creacion del volumen hasta que un pod realmente lo necesite, asegurando que el volumen se cree en la misma availability zone que el pod. Sin esto, podes terminar con un volumen en una AZ y un pod que necesita correr en otra.


Consideraciones de costo

EKS no es barato, especialmente comparado con ECS con Fargate para workloads chicos. Esto es lo que estas pagando:


  • Control plane: $0.10/hora ($73/mes). Esto es fijo sin importar cuantos nodos corras.
  • Worker nodes: Precios estandar de EC2. Un t3.medium (2 vCPU, 4 GB) cuesta aproximadamente $30/mes on-demand.
  • Instancias Spot: Hasta 90% mas baratas que on-demand, pero pueden ser interrumpidas. Karpenter hace facil usar Spot diversificando entre tipos de instancia. Genial para workloads stateless, no recomendado para bases de datos.
  • NAT gateway: $32/mes mas transferencia de datos. Este es generalmente el costo solapado que sorprende a la gente. Usa un solo NAT gateway para dev, uno por AZ para produccion.
  • Load balancers: Los ALBs cuestan aproximadamente $16/mes mas transferencia de datos. Cada recurso Ingress puede compartir un solo ALB usando IngressGroups para evitar provisionar uno por servicio.
  • Transferencia de datos: El trafico inter-AZ cuesta $0.01/GB en cada direccion. La comunicacion pod-a-pod entre AZs se acumula en arquitecturas de microservicios con mucha comunicacion.

Tips para ahorrar costos:


  • Usa Karpenter con instancias Spot para workloads stateless. Diversifica entre muchos tipos de instancia para reducir tasas de interrupcion.
  • Dimensiona bien tus nodos. Karpenter ayuda aca eligiendo el tipo de instancia optimo para tu mix de workloads.
  • Consolida ALBs usando anotaciones de IngressGroup para que multiples servicios compartan un ALB.
  • Usa un solo NAT gateway para entornos de no-produccion.
  • Pone resource requests y limits en cada pod para que Karpenter pueda empaquetar eficientemente.
  • Considera Savings Plans o Reserved Instances para capacidad base que sabes que siempre vas a necesitar.

Un entorno dev EKS minimo (control plane + 2 nodos t3.medium + NAT gateway + ALB) cuesta aproximadamente $180/mes. Un setup de produccion con mas nodos, NAT multi-AZ, y monitoreo va a ser significativamente mas. Compara esto con ECS con Fargate donde solo pagas por el compute que tus containers realmente usan.


Poniendo todo junto

Recorramos el flujo completo de provisionamiento:


# Inicializar Terraform
cd eks-cluster/terraform
terraform init

# Revisar el plan
terraform plan -out=tfplan

# Aplicar (esto tarda 15-20 minutos, mayormente la creacion del cluster EKS)
terraform apply tfplan

# Configurar kubectl
aws eks update-kubeconfig --region us-east-1 --name devops-zero-to-hero

# Verificar el cluster
kubectl get nodes
kubectl get pods -n kube-system

# Aplicar recursos de Karpenter
kubectl apply -f karpenter-nodepool.yaml

# Deployear la aplicacion
kubectl create namespace task-api
helm install task-api oci://ghcr.io/your-org/charts/task-api \
  --version 0.1.0 \
  --namespace task-api \
  -f values-eks.yaml

# Verificar que todo esta corriendo
kubectl get all -n task-api

Despues de unos 20 minutos, vas a tener un cluster EKS completamente funcional con managed node groups, Karpenter para escalado dinamico, el AWS Load Balancer Controller para provisionamiento automatico de ALBs, IRSA para permisos de AWS seguros a nivel pod, y el EBS CSI driver para storage persistente.


Limpieza

Si estas siguiendo y no queres mantener el cluster corriendo, tiralo abajo:


# Eliminar recursos de la aplicacion primero
helm uninstall task-api -n task-api
kubectl delete -f karpenter-nodepool.yaml

# Destruir todo con Terraform
terraform destroy

Siempre elimina los recursos de Kubernetes antes de destruir la infraestructura. Si destruis la VPC mientras los ALBs todavia existen, Terraform se va a colgar esperando que los load balancers se eliminen, y vas a tener que limpiarlos manualmente en la consola de AWS.


Notas finales

EKS te da todo el poder de Kubernetes sin la carga operacional de gestionar el control plane. En este articulo provisionamos un cluster completo con Terraform, configuramos managed node groups para compute base, configuramos Karpenter para autoscaling inteligente, usamos IRSA para permisos de AWS seguros a nivel pod, instalamos el AWS Load Balancer Controller para gestion automatica de ALBs, y deployeamos nuestra API TypeScript del Helm chart que construimos en el articulo anterior.


El trade-off comparado con ECS es complejidad y costo. EKS requiere mas conocimiento de infraestructura, mas partes moviles, y un costo base incluso cuando nada esta corriendo. Pero a cambio obtenes todo el ecosistema de Kubernetes, portabilidad entre nubes, y la capacidad de manejar workloads complejos que serian dificiles de modelar en ECS.


En el proximo articulo vamos a meternos con monitoreo y observabilidad, porque tener un cluster corriendo es solo el comienzo. Necesitas saber que esta pasando adentro.


Espero que te haya resultado util y lo hayas disfrutado! Hasta la proxima!


Errata

Si encontras algun error o tenes alguna sugerencia, por favor mandame un mensaje para que se corrija.

Tambien podes revisar el codigo fuente y los cambios en las fuentes aca



$ Comentarios

Online: 0

Por favor inicie sesión para poder escribir comentarios.

2026-05-27 | Gabriel Garrido