La lección anterior terminó con una pregunta que alguien del equipo iba a hacer esta misma semana: todo el mundo habla de Kubernetes. Es el estándar de facto de la orquestación de contenedores, tiene un ecosistema enorme, funciona igual en cualquier nube y hay mucha gente que sabe usarlo. Si MercadoFresco acaba de mover toda su tienda a contenedores y se ha quedado en ECS, ¿se está equivocando?

Esta lección responde con datos. Verás qué es Kubernetes de verdad y qué resuelve que ECS no resuelve, qué parte gestiona Amazon EKS y qué sigue siendo trabajo tuyo, cómo se despliega la tienda de MercadoFresco con manifiestos completos, y cuál es el coste que casi nunca aparece en las comparativas: el trabajo recurrente de las actualizaciones de versión. Al final, la decisión razonada de MercadoFresco y los criterios objetivos que la cambiarían. Y con ella, el cierre del módulo y el salto al módulo 11.

Aviso de coste. El plano de control de EKS cuesta 0,10 USD por hora y clúster, unos 73 USD al mes, independientemente del tamaño. Con soporte extendido —cuando la versión de Kubernetes sale del soporte estándar— pasa a 0,60 USD por hora, unos 438 USD al mes, y es un cargo automático que sorprende a mucha gente. A eso se suman los nodos (EC2 o Fargate), el balanceador y los complementos. Un clúster de pruebas olvidado es de los residuos más caros del curso: sigue la limpieza del final. Datos e identificadores ficticios.

Contenido

  1. Qué es Kubernetes y qué resuelve que ECS no
  2. Vocabulario mínimo pero real
  3. El modelo declarativo y el bucle de reconciliación
  4. Qué gestiona EKS y qué sigue siendo tuyo
  5. Opciones de cómputo: nodos, Fargate y Auto Mode
  6. Crear un clúster con eksctl y con CDK
  7. kubectl y el acceso al clúster
  8. Autenticación: entradas de acceso frente a aws-auth
  9. IRSA y EKS Pod Identity: permisos de AWS para un pod
  10. Los manifiestos de la tienda de MercadoFresco
  11. Ingress y el AWS Load Balancer Controller
  12. Autoescalado: HPA, Cluster Autoscaler y Karpenter
  13. Complementos: por qué un EKS vacío no sirve para nada
  14. Actualizaciones de versión: el coste oculto
  15. Observabilidad: Container Insights, Prometheus y Grafana
  16. ECS frente a EKS: la comparativa honesta
  17. La decisión de MercadoFresco y qué la cambiaría
  18. Coste y limpieza
  19. Errores comunes y consejos
  20. Ejercicios
  21. Cierre del módulo y transición al módulo 11

Qué es Kubernetes y qué resuelve que ECS no

Kubernetes es un orquestador de contenedores de código abierto, nacido en Google y donado a la Cloud Native Computing Foundation. Hace lo mismo que ECS —mantener contenedores en ejecución, repartirlos entre máquinas, reponerlos, exponerlos por red— con una diferencia fundamental de diseño: Kubernetes es una plataforma extensible con una API propia, no un servicio de un proveedor.

Esa diferencia produce tres capacidades que ECS no tiene:

  • Portabilidad real. Un manifiesto de Kubernetes funciona en EKS, en Google Kubernetes Engine, en Azure Kubernetes Service, en un clúster propio y en el portátil de Luis con kind. Una definición de tarea de ECS solo funciona en AWS. Para MercadoFresco esto no es urgente hoy, pero es el argumento con más peso a favor de EKS.
  • Un ecosistema que resuelve problemas que tú no quieres resolver. Existen miles de componentes listos para instalar: gestión de certificados (cert-manager), despliegues progresivos (Argo Rollouts, Flagger), GitOps (Argo CD, Flux), mallas de servicio (Istio, Linkerd), operadores que gestionan bases de datos, colas o motores de búsqueda como si fueran recursos nativos. En ECS, casi todo eso lo pone AWS o lo escribes tú.
  • Extensibilidad mediante la API. Con CRD (Custom Resource Definitions) y operadores se pueden crear tipos de recurso propios. Es lo que permite escribir kubectl apply sobre un fichero que declara «quiero un clúster de Kafka con tres réplicas» y que un operador lo construya y lo mantenga. ECS no tiene equivalente.

Y produce un coste que hay que nombrar con la misma claridad: Kubernetes es sustancialmente más complejo. Hay más conceptos, más piezas que instalar, más configuración que puede estar mal y más versiones que actualizar. La pregunta correcta no es «¿cuál es mejor?» sino «¿el problema que tengo justifica esa complejidad?».

Vocabulario mínimo pero real

Estos son los objetos que hay que conocer para leer un manifiesto sin perderse. La columna de la derecha ancla cada uno a lo que ya sabes de los módulos anteriores.

Objeto Qué es Equivalente aproximado
Clúster El conjunto: plano de control más nodos Clúster de ECS
Nodo (node) Una máquina que ejecuta cargas; suele ser una instancia EC2 Instancia del clúster de ECS
Pod La unidad mínima: uno o varios contenedores que comparten red y almacenamiento Tarea de ECS
ReplicaSet Garantiza que hay N pods iguales La parte del servicio de ECS que repone
Deployment Gestiona ReplicaSets y orquesta actualizaciones progresivas Servicio de ECS
Service Nombre e IP estables para un conjunto de pods Cloud Map o ECS Service Connect
Ingress Enrutado HTTP externo hacia Services Reglas de un ALB (03-03)
Namespace Partición lógica dentro del clúster Agrupación por proyecto o entorno
ConfigMap Configuración no sensible como pares clave-valor environment de la definición de tarea
Secret Configuración sensible (codificada en base64, no cifrada por omisión) secrets desde Secrets Manager
DaemonSet Un pod por nodo No existe en Fargate; sí en ECS sobre EC2
Job / CronJob Tarea que termina, puntual o programada run-task / EventBridge Scheduler

Dos advertencias sobre esa tabla, porque son fuente constante de errores:

  • Un Secret de Kubernetes no está cifrado por omisión. Está codificado en base64, que no es cifrado: cualquiera con permiso de lectura sobre el namespace lo ve en claro. Se resuelve con cifrado en etcd mediante KMS y, mejor aún, con el controlador CSI de secretos que trae los valores desde Secrets Manager (más abajo).
  • El Deployment no es el servicio y el Service no es el Deployment. El Deployment gestiona los pods; el Service les da un nombre estable. En ECS, ambas cosas están dentro del objeto «servicio», y esa fusión es una de las razones por las que ECS es más simple y menos flexible.

El modelo declarativo y el bucle de reconciliación

Kubernetes no ejecuta órdenes: compara estados. Tú declaras el estado deseado y un conjunto de controladores trabaja continuamente para que el estado real coincida.

graph LR
  U["kubectl apply -f<br/>manifiesto.yaml"] --> API["Servidor de API<br/>plano de control"]
  API --> ETCD[("etcd<br/>estado deseado")]
  API --> CM["Controladores<br/>(Deployment, ReplicaSet...)"]
  CM -->|"compara deseado<br/>con real"| API
  CM --> SCH["Planificador<br/>elige nodo"]
  SCH --> KL["kubelet en el nodo"]
  KL --> POD["Pods en ejecucion"]
  POD -->|"estado real"| API
  API -.->|"diferencia detectada"| CM

La consecuencia práctica es que borrar un pod a mano no sirve de nada: el ReplicaSet detecta que faltan réplicas y crea otro en segundos. Es la misma lógica del desiredCount de ECS, generalizada a todos los objetos del sistema y expuesta como una API que cualquiera puede extender.

Y la consecuencia cultural es más importante: en Kubernetes el manifiesto es la infraestructura. Esto encaja de forma natural con lo aprendido en el módulo 9 y lleva a GitOps: un repositorio Git es la fuente de verdad y un agente en el clúster (Argo CD o Flux) aplica lo que haya en él, revirtiendo cualquier cambio manual. Es una forma de trabajar más rigurosa que la del módulo 8, y también una pieza más que instalar, actualizar y operar.

Qué gestiona EKS y qué sigue siendo tuyo

Amazon EKS es Kubernetes gestionado: AWS opera el plano de control y tú operas todo lo demás. La lista concreta importa porque casi todas las decepciones con EKS vienen de esperar más de lo que ofrece.

AWS gestiona Detalle
El plano de control Servidor de API, planificador, controladores y etcd, replicados en tres AZ
Su disponibilidad y escalado Se dimensiona solo; no hay maestros que administrar
Las copias de etcd Automáticas; no las ves ni las gestionas
El proceso de actualización de versión Un botón o un comando, con validaciones previas
La integración con IAM Autenticación de usuarios y roles de AWS contra el clúster
Los complementos gestionados VPC CNI, CoreDNS, kube-proxy, EBS CSI y otros, con versiones compatibles
Sigue siendo tuyo Detalle
Los nodos Su tipo, su AMI, su parcheo y su reemplazo (salvo Fargate o Auto Mode)
Decidir cuándo actualizar Y probarlo, y ejecutarlo, tres o cuatro veces al año
La compatibilidad de tus manifiestos Las API de Kubernetes se retiran entre versiones y rompen cosas
Todo el software del clúster Controlador de balanceadores, autoescalador, controlador de secretos, métricas
La red VPC, subredes, grupos de seguridad, asignación de IP a pods
La seguridad dentro del clúster RBAC, políticas de red, políticas de seguridad de pods
La observabilidad Métricas, registros y trazas, igual que en ECS

La frase que hay que retener: EKS gestiona el plano de control, no el clúster. Un clúster de EKS recién creado no puede desplegar nada útil: le falta el controlador de balanceadores, el autoescalador, el controlador de secretos y las métricas. Esa distancia entre «clúster creado» y «clúster operativo» es donde vive la mayor parte del trabajo.

Opciones de cómputo: nodos, Fargate y Auto Mode

Los pods tienen que ejecutarse en algún sitio, y EKS ofrece cuatro modelos.

Opción Quién gestiona los nodos Parcheo Escalado Cuándo tiene sentido
Grupos de nodos gestionados AWS crea y sustituye; tú lanzas la actualización Guiado por AWS, sin corte si lo configuras Cluster Autoscaler o Karpenter El caso general
Nodos autogestionados Tú, con un ASG propio Todo tuyo Tuyo AMI personalizada, requisitos muy concretos
EKS sobre Fargate Nadie: no hay nodos Ninguno Por pod Cargas aisladas, sin DaemonSet
EKS Auto Mode AWS gestiona nodos, escalado y parches AWS, con reciclado automático Integrado, basado en Karpenter Quien quiere EKS con la operación mínima

EKS sobre Fargate funciona con perfiles de Fargate: se declara qué namespaces o etiquetas hacen que un pod se ejecute sin nodo. Tiene limitaciones importantes que hay que conocer antes de contar con él: no admite DaemonSet, ni almacenamiento persistente distinto de EFS, ni hostPort, ni pods privilegiados, y un pod por tarea de Fargate con el redondeo de recursos que eso implica. La consecuencia práctica es que muchos complementos —que se instalan como DaemonSet— no pueden ejecutarse sobre Fargate, así que casi siempre hace falta al menos un grupo de nodos pequeño para ellos.

EKS Auto Mode es la respuesta de AWS al argumento de esta lección: gestiona los nodos, el escalado, el parcheo y varios complementos esenciales, reciclando los nodos automáticamente cada cierto tiempo. Reduce mucho el trabajo operativo a cambio de un recargo sobre el precio del cómputo y de menos control. Lo que no elimina es la parte que más pesa: sigues decidiendo y ejecutando las actualizaciones de versión de Kubernetes y sigues siendo responsable de que tus manifiestos sean compatibles.

Crear un clúster con eksctl y con CDK

eksctl es la herramienta oficial de la comunidad y la vía más rápida. Es declarativa: se le da un fichero y construye la VPC —o usa la tuya—, el plano de control, los grupos de nodos y los complementos.

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: eks-mercadofresco
  region: eu-west-1
  version: "1.31"
  tags:
    Proyecto: mercadofresco
    Entorno: desarrollo
    Componente: orquestacion
    Propietario: plataforma
    CentroCoste: tecnologia

vpc:
  id: vpc-mercadofresco                     # se reutiliza la VPC del modulo 3
  subnets:
    private:
      eu-west-1a: { id: snet-mercadofresco-app-a }
      eu-west-1b: { id: snet-mercadofresco-app-b }
    public:
      eu-west-1a: { id: snet-mercadofresco-publica-a }
      eu-west-1b: { id: snet-mercadofresco-publica-b }
  clusterEndpoints:
    publicAccess: true                      # restringido por CIDR mas abajo
    privateAccess: true
  publicAccessCIDRs: ["203.0.113.0/24"]     # solo la red corporativa

managedNodeGroups:
  - name: ng-mercadofresco-general
    instanceTypes: ["m6g.large"]            # Graviton, como en 10-02
    amiFamily: AmazonLinux2023
    minSize: 2
    maxSize: 8
    desiredCapacity: 3
    privateNetworking: true                 # nodos solo en subredes privadas
    volumeSize: 50
    volumeType: gp3
    labels: { rol: general }
    updateConfig: { maxUnavailablePercentage: 25 }

addons:
  - name: vpc-cni
    version: latest
    configurationValues: '{"env":{"ENABLE_PREFIX_DELEGATION":"true"}}'
  - name: coredns
  - name: kube-proxy
  - name: aws-ebs-csi-driver
  - name: eks-pod-identity-agent

iam:
  withOIDC: true                            # imprescindible para IRSA
  serviceAccounts:
    - metadata: { name: aws-load-balancer-controller, namespace: kube-system }
      wellKnownPolicies: { awsLoadBalancerController: true }

cloudWatch:
  clusterLogging:
    enableTypes: ["api", "audit", "authenticator"]
eksctl create cluster -f eks-mercadofresco.yaml     # de 15 a 20 minutos

Cuatro decisiones de ese fichero que merecen atención:

  • publicAccessCIDRs restringido. Por omisión el endpoint del servidor de API es accesible desde cualquier IP de internet —protegido por autenticación, pero expuesto—. Restringirlo a la red corporativa o pasar a solo acceso privado es una de las primeras cosas que revisa una auditoría.
  • privateNetworking: true. Los nodos en subredes privadas, coherente con todo lo que hizo MercadoFresco en el módulo 3 y con lo aprendido en 10-02 sobre endpoints de VPC. Los mismos endpoints de la lección anterior son necesarios aquí.
  • withOIDC: true. Sin el proveedor OIDC no hay IRSA, y sin IRSA la única forma de dar permisos de AWS a un pod es el rol del nodo, que es un mal grave: todos los pods de ese nodo heredarían esos permisos.
  • ENABLE_PREFIX_DELEGATION. Sin esto, cada pod consume una IP secundaria de la ENI del nodo y el límite de pods por nodo se alcanza mucho antes de agotar CPU o memoria. Es el equivalente en EKS del awsvpcTrunking de 10-01, y sorprende igual.

En CDK (09-02), el equivalente es más corto pero exige entender lo mismo:

const cluster = new eks.Cluster(this, 'MercadoFrescoEks', {
  clusterName: 'eks-mercadofresco',
  version: eks.KubernetesVersion.V1_31,
  kubectlLayer: new KubectlV31Layer(this, 'CapaKubectl'),
  vpc,
  vpcSubnets: [{ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }],
  defaultCapacity: 0,                                   // los nodos se declaran aparte
  authenticationMode: eks.AuthenticationMode.API,       // entradas de acceso, sin aws-auth
  clusterLogging: [eks.ClusterLoggingTypes.API, eks.ClusterLoggingTypes.AUDIT],
});

cluster.addNodegroupCapacity('General', {
  instanceTypes: [new ec2.InstanceType('m6g.large')],
  amiType: eks.NodegroupAmiType.AL2023_ARM_64_STANDARD,
  minSize: 2, maxSize: 8, desiredSize: 3,
  subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});

// Los manifiestos tambien son infraestructura: se despliegan con la pila.
cluster.addHelmChart('LoadBalancerController', {
  chart: 'aws-load-balancer-controller',
  repository: 'https://aws.github.io/eks-charts',
  namespace: 'kube-system',
  values: { clusterName: cluster.clusterName, serviceAccount: { create: false, name: 'aws-load-balancer-controller' } },
});

kubectl y el acceso al clúster

kubectl es el cliente. Se configura con un comando que escribe el contexto en ~/.kube/config usando las credenciales de AWS actuales:

aws eks update-kubeconfig --name eks-mercadofresco --region eu-west-1
kubectl get nodes                 # los 3 nodos en Ready
kubectl get pods -A               # los pods del sistema: coredns, aws-node, kube-proxy
kubectl config current-context    # verificar SIEMPRE contra que cluster apuntas

El último comando parece trivial y no lo es: kubectl apunta al último clúster configurado, y kubectl delete deployment tienda no pregunta en qué entorno estás. Es el cdk destroy --all de 09-02 con otra forma. La disciplina que lo evita es doble: cuentas separadas por entorno (09-04), de modo que las credenciales de desarrollo no puedan tocar el clúster de producción, y un indicador del contexto activo en el prompt del terminal.

Autenticación: entradas de acceso frente a aws-auth

Kubernetes tiene su propio sistema de permisos, RBAC, con Role, ClusterRole, RoleBinding y ClusterRoleBinding. EKS conecta ese sistema con IAM: IAM dice quién eres, RBAC dice qué puedes hacer dentro del clúster.

Históricamente esa conexión se hacía con un ConfigMap llamado aws-auth, y era una fuente notoria de incidentes: un error de sintaxis al editarlo podía dejar a todo el mundo fuera del clúster de forma irreversible, sin más salida que recrearlo. Hoy la forma correcta son las entradas de acceso (access entries), que son API de AWS: se crean con la CLI o con CDK, se auditan en CloudTrail y no dependen de editar YAML a mano.

# El equipo de plataforma, administrador del cluster
aws eks create-access-entry --cluster-name eks-mercadofresco \
  --principal-arn arn:aws:iam::333344445555:role/AWSReservedSSO_AdministracionPlataforma_abc \
  --type STANDARD
aws eks associate-access-policy --cluster-name eks-mercadofresco \
  --principal-arn arn:aws:iam::333344445555:role/AWSReservedSSO_AdministracionPlataforma_abc \
  --access-scope type=cluster \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy

# Los desarrolladores, solo en su namespace y sin poder borrar el cluster
aws eks associate-access-policy --cluster-name eks-mercadofresco \
  --principal-arn arn:aws:iam::333344445555:role/AWSReservedSSO_DesarrolloCompleto_def \
  --access-scope type=namespace,namespaces=mercadofresco \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSEditPolicy

Los conjuntos de permisos son los de 09-04: AdministracionPlataforma, DesarrolloCompleto y AnalisisDatos. La correspondencia queda limpia: el mismo mecanismo de identidad que gobierna las cinco cuentas gobierna también el clúster, sin usuarios de Kubernetes paralelos ni credenciales aparte. Es una de las ventajas reales de EKS frente a un Kubernetes autogestionado.

IRSA y EKS Pod Identity: permisos de AWS para un pod

El equivalente exacto del rol de tarea de ECS (10-01). Un pod de la tienda necesita enviar a cola-mercadofresco-pedidos y leer mercadofresco-carritos, y necesita hacerlo con permisos propios, no con los del nodo.

Mecanismo Cómo funciona Ventajas Inconvenientes
Rol del nodo Todos los pods heredan el rol de la instancia Cero configuración Inaceptable: cualquier pod hace todo lo que hace cualquier otro
IRSA (roles para cuentas de servicio) Proveedor OIDC del clúster + política de confianza que lo referencia Maduro, funciona en todas partes Configuración por rol algo laboriosa, ligada a un clúster
EKS Pod Identity Un agente en el nodo y una asociación entre cuenta de servicio y rol Más simple, reutilizable entre clústeres, sin OIDC Requiere el complemento; no aplica a EKS sobre Fargate

Con Pod Identity, que es la opción recomendada hoy:

aws eks create-pod-identity-association --cluster-name eks-mercadofresco \
  --namespace mercadofresco --service-account sa-tienda \
  --role-arn arn:aws:iam::333344445555:role/rol-pod-mercadofresco-tienda

Y la política de confianza del rol, que es lo que cambia respecto a un rol normal:

{"Version": "2012-10-17", "Statement": [{
  "Effect": "Allow",
  "Principal": { "Service": "pods.eks.amazonaws.com" },
  "Action": ["sts:AssumeRole", "sts:TagSession"]
}]}

La política de permisos es literalmente la misma que la del rol de tarea de ECS en 10-01: sqs:SendMessage sobre la cola, dynamodb:*Item sobre la tabla de carritos y s3:GetObject sobre las fotos del catálogo. El mismo principio de mínimo privilegio, distinta forma de anclarlo a la carga.

Los manifiestos de la tienda de MercadoFresco

Aquí está lo que en ECS era una definición de tarea más un servicio. Empezamos por el Namespace, la ServiceAccount y el ConfigMap:

apiVersion: v1
kind: Namespace
metadata:
  name: mercadofresco
  labels: { proyecto: mercadofresco, entorno: desarrollo }
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sa-tienda
  namespace: mercadofresco
# Sin anotaciones: con Pod Identity la asociacion se hace desde la API de AWS.
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: cfg-tienda
  namespace: mercadofresco
data:
  ENTORNO: "desarrollo"
  COLA_PEDIDOS: "cola-mercadofresco-pedidos"
  TABLA_CARRITOS: "mercadofresco-carritos"

El Deployment, que es el corazón del despliegue:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tienda
  namespace: mercadofresco
  labels: { app: tienda, proyecto: mercadofresco }
spec:
  replicas: 3
  revisionHistoryLimit: 5                      # cuantas revisiones se pueden revertir
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0                        # equivale a minimumHealthyPercent 100 de ECS
      maxSurge: 2                              # equivale a maximumPercent 200
  selector:
    matchLabels: { app: tienda }
  template:
    metadata:
      labels: { app: tienda, proyecto: mercadofresco }
    spec:
      serviceAccountName: sa-tienda
      # Reparte los pods entre AZ: el equivalente de spread por AZ en ECS.
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels: { app: tienda }
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        fsGroup: 10001
      terminationGracePeriodSeconds: 30        # equivale a stopTimeout de ECS
      containers:
        - name: tienda
          image: 555566667777.dkr.ecr.eu-west-1.amazonaws.com/mercadofresco/tienda@sha256:9c1e...f4a2
          ports:
            - containerPort: 8080
              name: http
          envFrom:
            - configMapRef: { name: cfg-tienda }
          env:
            - name: BD_CONTRASENA
              valueFrom:
                secretKeyRef: { name: sec-tienda-rds, key: password }
          resources:
            requests: { cpu: "500m", memory: "1Gi" }   # lo que el planificador reserva
            limits:   { cpu: "1000m", memory: "2Gi" }  # el techo duro
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities: { drop: ["ALL"] }
          volumeMounts:
            - { name: temporal, mountPath: /tmp }
          startupProbe:                        # da tiempo a arrancar sin que liveness mate
            httpGet: { path: /salud, port: 8080 }
            periodSeconds: 5
            failureThreshold: 12               # hasta 60 s para el primer arranque
          readinessProbe:                      # decide si recibe trafico
            httpGet: { path: /salud, port: 8080 }
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
          livenessProbe:                       # decide si se reinicia el contenedor
            httpGet: { path: /salud, port: 8080 }
            periodSeconds: 20
            timeoutSeconds: 3
            failureThreshold: 3
          lifecycle:
            preStop:
              exec: { command: ["sleep", "5"] } # margen para que el ALB desregistre
      volumes:
        - name: temporal
          emptyDir: { sizeLimit: 1Gi }

Los puntos que hay que entender de ese manifiesto:

  • Las tres sondas son tres preguntas distintas y confundirlas causa incidentes. startupProbe responde «¿ha terminado de arrancar?» y suspende a las otras dos hasta que acierta. readinessProbe responde «¿puede recibir tráfico ahora?» y su fallo saca al pod del balanceo sin matarlo. livenessProbe responde «¿está colgado?» y su fallo reinicia el contenedor. El error clásico es apuntar livenessProbe a un /salud que consulta Aurora: cuando la base de datos va lenta, Kubernetes reinicia todos los pods a la vez y convierte un problema de latencia en una caída total.
  • requests y limits no son lo mismo. requests es lo que el planificador reserva para decidir en qué nodo cabe el pod; limits es el techo. Si la CPU supera el límite, el pod se estrangula (se ralentiza); si la memoria lo supera, el kernel lo mata con OOM y verás OOMKilled en los eventos. Poner requests muy por debajo del uso real produce nodos sobrecomprometidos que se caen a la vez.
  • maxUnavailable: 0 con maxSurge: 2 es exactamente el 100/200 de ECS: nunca hay menos capacidad sana que la deseada, a costa de pagar unos minutos de capacidad doble durante el despliegue.
  • preStop con una espera corta resuelve el mismo problema que el drenaje del grupo de destino: cuando Kubernetes decide terminar un pod, el ALB puede tardar unos segundos en dejar de mandarle tráfico. Sin esa espera, se pierden peticiones en cada despliegue.
  • El Secret referenciado no se escribe a mano. MercadoFresco usa el controlador CSI de secretos con el proveedor de AWS, que monta mercadofresco/produccion/rds/mfadmin desde Secrets Manager y opcionalmente lo sincroniza como Secret de Kubernetes. Así el secreto no vive en Git ni en etcd en base64.

El Service, que da a los pods un nombre estable:

apiVersion: v1
kind: Service
metadata:
  name: svc-tienda
  namespace: mercadofresco
spec:
  type: ClusterIP                # solo interno: quien expone al exterior es el Ingress
  selector: { app: tienda }
  ports:
    - port: 80
      targetPort: 8080
      name: http

Ingress y el AWS Load Balancer Controller

Un Ingress por sí solo no hace nada: es una declaración que necesita un controlador que la traduzca a un balanceador real. En EKS, ese controlador es el AWS Load Balancer Controller, que crea y configura un ALB de verdad (03-03) a partir de las anotaciones.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ing-tienda
  namespace: mercadofresco
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip          # directo al pod, sin salto por el nodo
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
    alb.ingress.kubernetes.io/ssl-redirect: '443'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:eu-west-1:333344445555:certificate/abc-123
    alb.ingress.kubernetes.io/healthcheck-path: /salud
    alb.ingress.kubernetes.io/healthcheck-interval-seconds: '15'
    alb.ingress.kubernetes.io/wafv2-acl-arn: arn:aws:wafv2:eu-west-1:333344445555:regional/webacl/waf-mercadofresco-cdn/abc
    alb.ingress.kubernetes.io/tags: Proyecto=mercadofresco,Entorno=desarrollo,Componente=tienda,Propietario=plataforma,CentroCoste=tecnologia
    alb.ingress.kubernetes.io/group.name: mercadofresco   # varios Ingress comparten un ALB
spec:
  ingressClassName: alb
  rules:
    - host: tienda.mercadofresco.example
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: svc-tienda
                port: { number: 80 }

Tres observaciones que conectan con módulos anteriores. target-type: ip hace que el ALB envíe tráfico directamente a la IP del pod, sin pasar por el NodePort del nodo: es el equivalente exacto del grupo de destino de tipo ip de 10-01, y ahorra un salto de red. group.name permite que varios Ingress compartan un único ALB, lo que importa porque cada ALB cuesta unos 17 USD al mes de coste fijo y crear uno por servicio se paga rápido. Y las anotaciones de WAF y certificado demuestran que, por debajo, sigue siendo el mismo ALB del módulo 3 con las mismas protecciones del módulo 4: EKS cambia cómo se declara, no qué se obtiene.

Autoescalado: HPA, Cluster Autoscaler y Karpenter

En Kubernetes hay dos escalados y son independientes: el de pods y el de nodos. Confundirlos es la causa de la queja «he configurado el autoescalado y no escala».

El HorizontalPodAutoscaler es el equivalente del autoescalado del servicio de ECS:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: hpa-tienda
  namespace: mercadofresco
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: tienda }
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource: { name: cpu, target: { type: Utilization, averageUtilization: 65 } }
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30           # crecer rapido, como en 10-01
      policies: [{ type: Percent, value: 100, periodSeconds: 30 }]
    scaleDown:
      stabilizationWindowSeconds: 300          # decrecer despacio
      policies: [{ type: Percent, value: 25, periodSeconds: 60 }]

Escalar por peticiones por destino —que en 10-01 se demostró mejor que la CPU— exige aquí instalar además KEDA o el adaptador de métricas de CloudWatch, porque el HPA nativo solo entiende CPU y memoria. Es un ejemplo pequeño y representativo del patrón general: lo que en ECS es un parámetro, en Kubernetes suele ser un componente más que instalar y mantener.

Para los nodos hay dos opciones:

Cluster Autoscaler Karpenter
Cómo decide Ajusta el tamaño de grupos de nodos predefinidos Elige el tipo de instancia adecuado para los pods pendientes
Velocidad Minutos Segundos hasta lanzar; el arranque sigue siendo de EC2
Eficiencia Limitada por los tipos del grupo Alta: consolida y elige el tamaño óptimo
Spot Posible, con grupos aparte Nativo, con sustitución al recibir el aviso
Complejidad Baja, es lo tradicional Media: otro controlador que instalar y actualizar

Karpenter es hoy la opción recomendada y es genuinamente buena: observa los pods que no caben, decide qué instancia los alojaría mejor, la lanza y, cuando sobra, consolida las cargas y apaga nodos. Pero conviene ver el conjunto con perspectiva: para conseguir en EKS lo que en Fargate viene puesto —que la capacidad aparezca cuando hace falta y desaparezca cuando no—, hay que instalar, configurar, actualizar y vigilar un controlador más. Y aun con Karpenter, el pod nuevo espera a que arranque una instancia EC2: los dos minutos que MercadoFresco quería eliminar reaparecen cuando no hay hueco.

Complementos: por qué un EKS vacío no sirve para nada

Un clúster recién creado tiene tres cosas y le faltan muchas. Esta es la lista mínima real para poder ejecutar la tienda de MercadoFresco:

Complemento Para qué ¿Gestionado por AWS?
VPC CNI (aws-node) Da a cada pod una IP de la VPC
CoreDNS Resolución DNS dentro del clúster
kube-proxy Reglas de red para los Services
EKS Pod Identity Agent Permisos de AWS por pod
EBS CSI / EFS CSI Volúmenes persistentes
AWS Load Balancer Controller Convierte Ingress en ALB No: lo instalas y actualizas tú
Secrets Store CSI + proveedor de AWS Trae secretos desde Secrets Manager No
Metrics Server Métricas que necesita el HPA No
Karpenter o Cluster Autoscaler Escalado de nodos No
CloudWatch Observability Container Insights y registros Sí (complemento)
KEDA (opcional) Escalado por métricas externas No
cert-manager (opcional) Certificados internos No

Los cinco marcados como «No» son software que MercadoFresco tendría que instalar con Helm, versionar, probar y actualizar, cada uno con su propio ciclo de vida y su propia matriz de compatibilidad con las versiones de Kubernetes. Ninguno es difícil por separado. La suma sí lo es, y es la parte de EKS que no aparece en las comparativas de una tarde.

En ECS, el equivalente de esa tabla entera es: nada. El balanceador es un parámetro del servicio, los secretos son un campo de la definición de tarea, el escalado es Application Auto Scaling y las métricas vienen con Container Insights.

Actualizaciones de versión: el coste oculto

Este es el argumento central de la lección y el que más peso tiene en la decisión final.

Kubernetes publica unas tres versiones menores al año. EKS da soporte estándar durante unos 14 meses por versión; después entra automáticamente en soporte extendido, que dura unos 12 meses más y cuesta 0,60 USD por hora en lugar de 0,10, es decir, unos 438 USD al mes por clúster en lugar de 73. Al terminar el extendido, AWS actualiza el clúster por su cuenta.

Lo que implica en trabajo real, tres o cuatro veces al año y por clúster:

  1. Leer las notas de la versión y detectar las API retiradas. Kubernetes retira versiones de API entre versiones menores, y un manifiesto que funcionaba deja de aplicarse. Herramientas como kubent ayudan a encontrarlos, pero la revisión es tuya.
  2. Comprobar la compatibilidad de cada complemento: el controlador de balanceadores, Karpenter, el CSI de secretos, KEDA, el CNI. Cada uno tiene su matriz, y a veces hay que actualizar el complemento antes que el clúster.
  3. Actualizar el plano de control, que tarda unos 25-40 minutos y no se puede revertir.
  4. Actualizar los nodos, sustituyendo instancias en tandas y drenando pods, con el riesgo habitual de que un PodDisruptionBudget mal puesto bloquee el drenaje.
  5. Probarlo todo en un clúster de preproducción antes, lo que implica mantener ese clúster, con su plano de control de 73 USD al mes y sus mismos complementos.
Actividad Frecuencia Esfuerzo estimado
Actualización de versión menor (preproducción y producción) 3 al año 2-4 días de una persona cada vez
Actualización de complementos fuera de ciclo 6-10 al año 2-4 horas cada vez
Seguimiento de CVE de los componentes del clúster Continuo 2-4 horas al mes
Total anual aproximado 20-30 días de trabajo de plataforma

Veinte o treinta días al año es más de un mes de una persona. Ese es el coste oculto de EKS, y no lo elimina Auto Mode: Auto Mode se encarga de los nodos, pero las versiones de Kubernetes, la compatibilidad de tus manifiestos y los complementos que instales siguen siendo tuyos.

En ECS ese trabajo simplemente no existe. No hay versión de ECS que actualizar; AWS evoluciona el servicio sin que nadie planifique nada. Es la comparación más honesta que se puede hacer entre los dos, y para un equipo de plataforma pequeño es decisiva.

Observabilidad: Container Insights, Prometheus y Grafana

Todo lo del módulo 5 sigue aplicándose; cambian las herramientas nativas del ecosistema.

  • Container Insights se activa con el complemento de observabilidad de CloudWatch y da métricas de clúster, nodo, pod y contenedor, más los registros de los contenedores en /aws/containerinsights/eks-mercadofresco/application. Es la vía más directa y la que menos piezas añade.
  • Amazon Managed Service for Prometheus es un Prometheus gestionado y compatible con PromQL. Encaja bien porque la mayoría de los componentes del ecosistema —el controlador de balanceadores, Karpenter, KEDA— ya exponen métricas en formato Prometheus. Se recoge con el agente de CloudWatch o con el recolector de OpenTelemetry, y se paga por muestras ingeridas y almacenadas.
  • Amazon Managed Grafana es el Grafana gestionado, con acceso mediante IAM Identity Center (09-04) y fuentes de datos hacia Prometheus, CloudWatch y X-Ray. Se paga por usuario activo al mes.
  • Los registros del plano de controlapi, audit, authenticator— se activan en el clúster y van a CloudWatch Logs. El de auditoría es el que responde «quién borró ese Deployment», y conviene activarlo desde el primer día porque no es retroactivo.

La observación honesta: la combinación Prometheus más Grafana es más potente que CloudWatch para métricas de Kubernetes, con mejor consulta y un catálogo de paneles listos. Y también es más piezas: dos servicios más que configurar, dos facturas más y dos sitios donde mirar durante un incidente. Para MercadoFresco, con los paneles mercadofresco-produccion y -negocio ya construidos en 05-01, la ganancia no compensa la fragmentación.

ECS frente a EKS: la comparativa honesta

Criterio Amazon ECS Amazon EKS
Curva de aprendizaje Días: cinco conceptos Semanas o meses: decenas de objetos y patrones
Coste del plano de control 0 USD 73 USD/mes, o 438 con soporte extendido
Portabilidad Ninguna: solo AWS Total: cualquier nube o servidor propio
Ecosistema Lo que ofrece AWS Enorme: operadores, GitOps, mallas, miles de gráficos de Helm
Extensibilidad Muy limitada CRD y operadores: se extiende la API
Integración con AWS Nativa y sin piezas Buena, pero mediante controladores que instalas tú
Actualizaciones Ninguna: AWS lo hace 3 al año, 20-30 días de trabajo anuales
Complementos a mantener Ninguno 5-8 componentes con su propio ciclo
Personal necesario Cualquier equipo con AWS Alguien que sepa Kubernetes, no que lo esté aprendiendo
Mercado laboral Menor Amplio: es una habilidad transferible
Fargate Sí, plenamente integrado Sí, con limitaciones (sin DaemonSet, sin EBS)
Multi-nube y híbrido No , incluido en instalaciones propias
Velocidad para llegar a producción Días Semanas
Techo de complejidad que soporta Medio Muy alto

Ninguna de las dos columnas es «la buena». La lectura correcta es: EKS compra flexibilidad, portabilidad y ecosistema, y lo paga con complejidad, personal y trabajo recurrente. Si necesitas lo que compra, el precio es razonable. Si no lo necesitas, has comprado trabajo.

La decisión de MercadoFresco y qué la cambiaría

MercadoFresco se queda en ECS con Fargate. Y conviene que la razón esté escrita, porque una decisión sin argumentos escritos se revisa cada seis meses en función de la última conferencia a la que fue alguien.

Los argumentos, en orden de peso:

  1. El equipo de plataforma es Marta y medio Luis. Veinte o treinta días al año de actualizaciones sobre dos personas que además llevan la red, la seguridad, el pipeline y las guardias no es un coste asumible: sería el 10-15 % de su capacidad total dedicado a mantener el orquestador, no la tienda.
  2. No hay ningún requisito que ECS no cubra. La tienda es un servicio HTTP con estado externo, unos trabajadores que consumen colas y unas funciones por eventos. Nada de eso necesita CRD, operadores, mallas de servicio ni programación avanzada de pods.
  3. No hay requisito multi-nube. MercadoFresco está en AWS y no hay plan ni presión comercial para cambiar eso. La portabilidad de Kubernetes es un seguro, y un seguro que no se va a usar es solo una prima.
  4. La velocidad importa. El módulo 10 ha llevado la tienda de EC2 a contenedores en semanas. Con EKS, esas mismas semanas habrían sido para levantar el clúster, los complementos y el aprendizaje del equipo, sin haber movido todavía la tienda.
  5. El coste directo también cuenta, aunque sea el argumento menor: 73 USD al mes por clúster y por entorno son unos 220 USD mensuales para desarrollo, preproducción y producción, frente a 0 USD de ECS. Es poco dinero comparado con el coste de personal, pero no es cero.

Y los criterios objetivos que cambiarían la decisión, escritos por adelantado para que la revisión futura sea una comprobación y no un debate:

Criterio Umbral concreto Por qué inclinaría la balanza
Experiencia previa del equipo Dos o más personas con Kubernetes en producción La curva deja de ser un coste y pasa a ser un activo
Requisito multi-nube o híbrido Un contrato, una normativa o un almacén con cómputo local Es lo único que ECS no puede dar de ninguna forma
Necesidad de operadores del ecosistema Un componente que solo existe como operador de Kubernetes Reimplementarlo cuesta más que adoptar EKS
Tamaño del equipo de plataforma Cuatro o más personas dedicadas Hay capacidad para absorber el trabajo recurrente
Número de servicios Más de 30-40 servicios con equipos distintos Los namespaces, RBAC y GitOps empiezan a ganar de verdad
Requisitos de programación avanzados Afinidades complejas, GPU compartidas, prioridad entre cargas El planificador de Kubernetes es muy superior

Mientras ninguno de esos umbrales se cumpla, la respuesta a «¿deberíamos pasarnos a Kubernetes?» es no, y aquí está por qué. El día que dos se cumplan, la respuesta cambia, y esta tabla dice exactamente cuándo.

Con un matiz que evita el error contrario: Luis debería aprender Kubernetes igualmente. Es una habilidad de mercado y da criterio para juzgar arquitecturas ajenas. Aprenderlo no obliga a adoptarlo, y adoptarlo sin saberlo es la peor combinación posible.

Coste y limpieza

Concepto Coste aproximado mensual
Plano de control, soporte estándar 73 USD por clúster
Plano de control, soporte extendido 438 USD por clúster
3 nodos m6g.large bajo demanda ≈ 165 USD
Volúmenes EBS de los nodos (3 × 50 GB gp3) ≈ 12 USD
ALB creado por el Ingress ≈ 17 USD más consumo
Managed Prometheus y Grafana Desde ~20 USD según uso y usuarios
Clúster de desarrollo mínimo y realista ≈ 270-290 USD/mes

Multiplicado por tres entornos, un EKS equivalente al montaje actual de MercadoFresco costaría del orden de 800 USD al mes frente a los aproximadamente 250 USD de ECS con Fargate y arm64 —y esa comparación no incluye los 20-30 días anuales de trabajo de plataforma, que es el coste mayor.

Limpieza, y aquí el orden importa más que en ninguna otra lección: los recursos de AWS creados desde dentro del clúster —el ALB del Ingress, los volúmenes EBS de los PersistentVolumeClaim— no se borran al borrar el clúster y quedan huérfanos facturando.

# 1. PRIMERO los objetos de Kubernetes que crean recursos de AWS
kubectl delete ingress --all -n mercadofresco     # borra el ALB
kubectl delete pvc --all -n mercadofresco         # borra los volumenes EBS
kubectl delete svc --all -n mercadofresco         # borra cualquier NLB de type LoadBalancer
# 2. Comprobar que de verdad han desaparecido antes de seguir
aws elbv2 describe-load-balancers --query 'LoadBalancers[?contains(LoadBalancerName,`k8s`)].LoadBalancerArn'
aws ec2 describe-volumes --filters Name=status,Values=available --query 'Volumes[].VolumeId'
# 3. Ahora si, el cluster completo (eksctl borra nodos, roles y la pila)
eksctl delete cluster --name eks-mercadofresco --region eu-west-1
# 4. Residuos habituales: grupos de registros y el proveedor OIDC
aws logs delete-log-group --log-group-name /aws/eks/eks-mercadofresco/cluster
aws iam list-open-id-connect-providers

Si se borra el clúster antes que los Ingress, el controlador que sabía borrar el ALB desaparece con él y el balanceador se queda facturando indefinidamente. Es el residuo más caro y más frecuente de EKS.

Errores Comunes y Consejos

Error: livenessProbe que consulta la base de datos. Cuando Aurora va lenta, Kubernetes reinicia todos los pods a la vez y una degradación se convierte en caída. Consejo: liveness comprueba solo que el proceso responde; las dependencias van en readiness.

Error: no poner startupProbe. Una aplicación que tarda 40 segundos en arrancar es reiniciada por liveness antes de terminar, en bucle. Consejo: startupProbe con failureThreshold generoso; suspende a las otras dos hasta que acierta.

Error: requests muy por debajo del uso real. Los nodos quedan sobrecomprometidos y caen varios pods a la vez. Consejo: requests cercano al percentil 95 real y limits con margen, medido con Container Insights.

Error: dar permisos de AWS mediante el rol del nodo. Todos los pods del nodo heredan todo. Consejo: Pod Identity o IRSA siempre, incluso en desarrollo, porque lo que se hace en desarrollo se copia a producción.

Error: editar aws-auth a mano. Un error de sintaxis deja a todo el mundo fuera del clúster sin vuelta atrás. Consejo: usa entradas de acceso (AuthenticationMode: API) y no toques ese ConfigMap.

Error: guardar Secret de Kubernetes en Git. Base64 no es cifrado. Consejo: controlador CSI de secretos con Secrets Manager, y cifrado de etcd con KMS activado en la creación del clúster.

Error: borrar el clúster antes que los Ingress. El ALB queda huérfano facturando. Consejo: el orden de limpieza de arriba, con verificación entre pasos.

Error: dejar que el clúster entre en soporte extendido sin darse cuenta. La factura se multiplica por seis en silencio. Consejo: una alarma en el calendario a los 12 meses de cada versión y un presupuesto por cuenta (11-04) que detecte el salto.

Error: un ALB por cada Ingress. Diez servicios, diez balanceadores, 170 USD al mes. Consejo: alb.ingress.kubernetes.io/group.name para compartir un ALB entre Ingress.

Error: elegir EKS por currículum. Es una razón real y sincera, y una mala razón técnica. Consejo: si el equipo quiere aprender Kubernetes, que monte un clúster de laboratorio con presupuesto propio; la producción se decide con los criterios de la tabla.

Consejo: si eliges EKS, elige también Karpenter y Auto Mode desde el principio. Empezar con Cluster Autoscaler y migrar después es trabajo duplicado.

Consejo: activa los registros de auditoría del plano de control el primer día. No son retroactivos, y son lo único que responde «quién borró ese Deployment».

Consejo: fija topologySpreadConstraints por zona en todo Deployment de producción. Sin ellos, el planificador puede poner las tres réplicas en la misma AZ y una zona caída te deja sin servicio.

Ejercicios

Ejercicio 1: traducir el servicio de trabajadores a Kubernetes

Los trabajadores de cola-mercadofresco-pedidos corren hoy en Fargate con 0,5 vCPU y 1 GB, escalando de 2 a 8 tareas según el retraso de la cola, con un reparto 1:4 entre bajo demanda y Spot. Escribe el equivalente en Kubernetes: (a) el Deployment completo, con las sondas que correspondan a un proceso sin servidor HTTP y el apagado ordenado; (b) cómo resuelves el autoescalado por profundidad de cola, indicando qué componente adicional hace falta y por qué el HPA nativo no basta; (c) cómo consigues el equivalente del reparto entre capacidad bajo demanda y Spot; y (d) enumera todas las piezas que has tenido que añadir al clúster para lograr lo que en ECS eran tres parámetros.

Ejercicio 2: el incidente de las sondas

Un viernes a las 19:20, con el pico en marcha, Aurora sufre una degradación y las consultas pasan de 20 ms a 900 ms. A los dos minutos, la tienda desaparece entera: todos los pods están en CrashLoopBackOff, el ALB no tiene destinos sanos y mercadofresco-alb-latencia-alta ha dado paso a un error total. La base de datos, mientras tanto, sigue respondiendo, lenta pero viva. Explica (a) la cadena causal exacta que ha convertido una degradación en una caída total; (b) qué configuración concreta del manifiesto es la culpable y por qué en ECS con la comprobación de estado del grupo de destino el resultado habría sido distinto; (c) cómo se rediseñan las tres sondas para que este incidente sea una degradación y no una caída; (d) qué mecanismo añadirías en el código de la tienda para degradar con elegancia; y (e) qué alarma habría avisado antes.

Ejercicio 3: la revisión de la decisión, dieciocho meses después

Han pasado dieciocho meses. MercadoFresco ha crecido: opera en cuatro ciudades, tiene 22 servicios en lugar de 3, el equipo de plataforma son ahora 4 personas de las que 2 han trabajado con Kubernetes en su empleo anterior, y el mayor cliente mayorista exige por contrato que su instancia del servicio de pedidos pueda ejecutarse en su propio centro de datos. Además, el equipo de datos quiere usar un operador de Kubernetes para gestionar sus flujos de procesamiento. Aplica la tabla de criterios de la lección y responde: (a) qué umbrales se cumplen ahora; (b) cuál es tu recomendación y con qué nivel de confianza; (c) si recomiendas migrar, cómo lo harías sin repetir los errores del plan de migración de 09-04; (d) qué se queda en ECS aunque migres; y (e) qué coste anual total estimarías para la decisión, incluyendo personal.

Soluciones

Solución 1

(a) El Deployment.

apiVersion: apps/v1
kind: Deployment
metadata: { name: trabajadores-pedidos, namespace: mercadofresco }
spec:
  replicas: 2
  selector: { matchLabels: { app: trabajadores-pedidos } }
  template:
    metadata: { labels: { app: trabajadores-pedidos } }
    spec:
      serviceAccountName: sa-trabajadores
      terminationGracePeriodSeconds: 120        # equivale a stopTimeout 120 de ECS
      topologySpreadConstraints:
        - { maxSkew: 1, topologyKey: topology.kubernetes.io/zone,
            whenUnsatisfiable: ScheduleAnyway, labelSelector: { matchLabels: { app: trabajadores-pedidos } } }
      containers:
        - name: trabajador
          image: 555566667777.dkr.ecr.eu-west-1.amazonaws.com/mercadofresco/trabajadores@sha256:71ab...c3d9
          resources:
            requests: { cpu: "500m", memory: "1Gi" }
            limits:   { cpu: "500m", memory: "1Gi" }
          securityContext: { runAsNonRoot: true, runAsUser: 10002, readOnlyRootFilesystem: true }
          # Sin HTTP: se ejecuta un comando que comprueba la marca de latido en /tmp.
          livenessProbe:
            exec: { command: ["python", "-m", "trabajadores.salud"] }
            periodSeconds: 30
            failureThreshold: 3
          startupProbe:
            exec: { command: ["python", "-m", "trabajadores.salud"] }
            periodSeconds: 5
            failureThreshold: 12
          volumeMounts: [{ name: temporal, mountPath: /tmp }]
      volumes: [{ name: temporal, emptyDir: {} }]

No hay readinessProbe y es deliberado: readiness decide si un pod recibe tráfico de un Service, y a un trabajador de cola nadie le manda tráfico. Poner una sería ruido sin efecto. liveness sí es imprescindible, por la misma razón que en 10-01: un trabajador colgado no muere, deja de consumir la cola y nadie se entera. El apagado ordenado es el mismo manejador de SIGTERM de aquel ejercicio, con terminationGracePeriodSeconds: 120 como equivalente del stopTimeout.

(b) Autoescalado por profundidad de cola. El HPA nativo solo entiende CPU y memoria de las métricas de recursos, y ya se vio que la CPU de un consumidor de cola no refleja el retraso. Hace falta KEDA, que instala un adaptador de métricas externas y trae un escalador específico para SQS:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: { name: escalado-trabajadores, namespace: mercadofresco }
spec:
  scaleTargetRef: { name: trabajadores-pedidos }
  minReplicaCount: 2
  maxReplicaCount: 8
  cooldownPeriod: 300
  triggers:
    - type: aws-sqs-queue
      metadata:
        queueURL: https://sqs.eu-west-1.amazonaws.com/333344445555/cola-mercadofresco-pedidos
        queueLength: "15"                 # mensajes por replica, como en 10-02
        awsRegion: eu-west-1
      identityOwner: operator

La alternativa sin KEDA es el adaptador de métricas de CloudWatch más un HPA con métrica externa, que es más artesanal. En cualquier caso, es un componente más que instalar, versionar y actualizar para conseguir lo que en ECS era una política de Application Auto Scaling.

(c) El equivalente del reparto bajo demanda / Spot. No hay un capacityProviderStrategy; se consigue con Karpenter declarando dos NodePool, uno con capacity-type: on-demand y otro con spot, y usando en el Deployment nodeAffinity preferida hacia Spot más topologySpreadConstraints sobre la etiqueta karpenter.sh/capacity-type para garantizar que al menos una réplica caiga en bajo demanda. También hace falta el manejador de interrupciones, que Karpenter incluye: escucha el aviso de dos minutos de EC2 y drena el nodo. Resultado: lo que en ECS eran dos líneas de --capacity-provider-strategy aquí son dos NodePool, reglas de afinidad, restricciones de topología y un controlador.

(d) Las piezas añadidas. Para igualar tres parámetros de ECS ha hecho falta: Karpenter (nodos y Spot), KEDA (escalado por cola), Metrics Server (base del HPA), EKS Pod Identity Agent (permisos de SQS del pod) y, si se quiere observabilidad equivalente, el complemento de CloudWatch Observability. Cinco componentes, cada uno con su versión, su compatibilidad con la versión de Kubernetes y su propio ciclo de actualización. Esa es exactamente la aritmética de la sección de complementos.

Solución 2

(a) La cadena causal. Aurora se degrada a 900 ms. El /salud de la tienda consulta la base de datos, así que empieza a tardar más de los 3 segundos de timeoutSeconds. La livenessProbe acumula 3 fallos consecutivos —unos 60 segundos con periodSeconds: 20— y Kubernetes reinicia el contenedor. Al reiniciarse, el pod tiene que arrancar de nuevo, precargar la caché del catálogo y volver a conectar con la misma Aurora lenta, con lo que falla también el arranque. Como todos los pods consultan la misma base de datos, esto le ocurre a los tres a la vez y de forma sincronizada. Los reinicios repetidos llevan a CrashLoopBackOff, con esperas crecientes entre intentos. Y hay un agravante: los reinicios masivos abren y cierran conexiones contra una Aurora ya saturada, empeorando la causa raíz. Un bucle de realimentación completo.

(b) La culpable y la diferencia con ECS. La culpable es la livenessProbe apuntando a un /salud que comprueba dependencias externas. En ECS, la comprobación de estado del grupo de destino tiene un efecto distinto: un destino no sano se saca del balanceo, y el servicio solo repone tareas si la tarea muere o si el despliegue lo exige. El equivalente de liveness en ECS es el healthCheck del contenedor, que la mayoría de la gente configura de forma más laxa o ni siquiera pone. Es decir: Kubernetes es más agresivo por omisión y te da una herramienta más peligrosa, y por eso exige entenderla. La diferencia no es que ECS sea mejor, es que Kubernetes te deja disparar más lejos.

(c) El rediseño de las tres sondas.

  • livenessProbe/vivo, un endpoint que solo confirma que el proceso responde y el bucle de eventos no está bloqueado. Sin tocar Aurora, sin tocar la caché, sin tocar la red. Con periodSeconds: 20 y failureThreshold: 5, para no reiniciar por un pico transitorio.
  • readinessProbe/listo, que sí comprueba las dependencias: conexión a Aurora y a ElastiCache. Su fallo saca al pod del balanceo pero no lo mata, así que cuando Aurora se recupera el pod vuelve a recibir tráfico sin haber reiniciado. Aquí el fallo es reversible y barato.
  • startupProbe/listo con failureThreshold amplio (12 × 5 s = 60 s), que cubre la precarga del catálogo y suspende a las otras dos mientras tanto.

Con ese diseño, el incidente del viernes habría sido: todos los pods marcados como no listos, el ALB devolviendo 503 en las peticiones que necesitan base de datos, los pods vivos y con su caché caliente, y recuperación inmediata al recuperarse Aurora. Degradación, no caída.

(d) Degradación con elegancia en el código. Que /listo distinga entre «no puedo hacer nada» y «puedo hacer parte». La tienda puede servir el catálogo desde mercadofresco-catalogo en ElastiCache aunque Aurora esté lenta; lo que no puede es confirmar pedidos. Con un interruptor de circuito (07-05) alrededor de las llamadas a Aurora, la tienda deja de intentarlo tras N fallos, devuelve un mensaje claro en el paso de pago y sigue sirviendo navegación. El cliente ve «no podemos confirmar pedidos ahora mismo» en lugar de una página en blanco, y Aurora deja de recibir la avalancha de reintentos que impedía su recuperación.

(e) La alarma que habría avisado antes. La de latencia de Aurora, ReadLatency y WriteLatency, con umbral sobre el percentil 99 y no sobre la media —la media tarda mucho en moverse—. Habría saltado al minuto de la degradación, antes de que la primera livenessProbe acumulara sus tres fallos, dando un margen de acción real. Complementariamente, una alarma sobre reinicios de pods (pod_number_of_container_restarts en Container Insights) habría identificado el bucle de realimentación de inmediato, que es la información que faltaba para diagnosticar en caliente.

Solución 3

(a) Umbrales que se cumplen. Cuatro de los seis:

Criterio ¿Se cumple? Detalle
Experiencia previa 2 de 4 personas con Kubernetes en producción
Multi-nube o híbrido Sí, y es contractual El mayorista exige ejecución en su centro de datos
Operadores del ecosistema El equipo de datos quiere un operador de flujos
Tamaño del equipo 4 personas dedicadas
Número de servicios Parcialmente 22, por debajo del umbral de 30-40 pero cerca
Programación avanzada No Nada lo exige todavía

(b) Recomendación. Sí, migrar a EKS, con confianza alta. El criterio decisivo es el segundo: el requisito híbrido contractual es lo único que ECS no puede satisfacer de ninguna manera, y no es una preferencia sino una obligación con un cliente. Los otros tres refuerzan que ahora el coste es asumible: hay experiencia, hay equipo y hay una necesidad concreta de ecosistema. La decisión de hace dieciocho meses no era errónea: era correcta con los datos de entonces, y esa es precisamente la utilidad de haber escrito los criterios por adelantado.

(c) Cómo migrar. Aplicando las lecciones de 09-04 y del propio módulo 10:

  1. Un clúster nuevo, no una conversión. El clúster de desarrollo primero, con Auto Mode o Karpenter desde el principio para no duplicar trabajo.
  2. Primero un servicio no crítico. El servicio de informes o el de notificaciones, no la tienda. Se aprende con algo cuyo fallo no cuesta pedidos.
  3. Los complementos antes que las cargas, con sus versiones fijadas en Git y desplegados con Helm desde el pipeline, nunca a mano.
  4. Convivencia real y prolongada. ECS y EKS a la vez durante meses, con el ALB repartiendo por pesos igual que en 10-01 y 10-02. Nada de una noche de corte.
  5. La tienda, la última, y con el mismo reparto 90/10 que se usó para pasar de EC2 a Fargate.
  6. Fecha límite explícita por fase, que es el error que 09-04 señalaba: las migraciones a medias duran años si nadie pone hitos.
  7. La instalación del cliente mayorista al final, cuando el equipo ya opere EKS con soltura, no como primer proyecto.

(d) Qué se queda en ECS aunque migres. Lo que no gana nada con el cambio: las Lambdas (-cobrar-pago, -reservar-stock, -generar-miniaturas, -asignar-reparto), que no son contenedores y siguen siendo la mejor opción para trabajo esporádico; las tareas programadas de carga a Redshift, que funcionan perfectamente con EventBridge Scheduler; y probablemente los trabajadores de las colas menos críticas durante bastante tiempo, porque migrarlos no aporta y sí consume atención. Migrar todo por coherencia estética es un error caro: la coherencia se mide en criterios, no en tecnologías.

(e) Coste anual estimado. Infraestructura: 3 clústeres × 73 USD × 12 = 2.628 USD de planos de control, más el diferencial de nodos frente a Fargate, que con Karpenter y Spot bien usados puede ser incluso favorable —digamos neutro—. Personal: 25 días al año de trabajo de plataforma en actualizaciones y mantenimiento del clúster, más un coste único de 2-3 meses-persona para la migración. Con un coste interno estimado de 400 USD por día, el recurrente son unos 10.000 USD al año y la migración, unos 20.000-25.000 USD de una vez. Total del primer año: del orden de 35.000 USD. Ese es el número que hay que poner delante de la dirección junto al contrato del cliente mayorista, porque la decisión no es técnica: es una inversión con una justificación comercial concreta.

Conclusión

MercadoFresco ha terminado el módulo con la tienda fuera de las máquinas y con una respuesta razonada a la pregunta de Kubernetes.

Tienes Kubernetes entendido de verdad: un orquestador extensible con API propia, cuyo valor real está en la portabilidad, el ecosistema y la extensibilidad con CRD y operadores; su vocabulario mínimo —Pod, ReplicaSet, Deployment, Service, Ingress, Namespace, ConfigMap y Secret, con la advertencia de que un Secret es base64 y no cifrado—; y el modelo declarativo con bucle de reconciliación que explica por qué borrar un pod a mano no sirve de nada y por qué GitOps encaja de forma natural. Tienes claro qué gestiona EKS —plano de control en tres AZ, etcd, el mecanismo de actualización y la integración con IAM— y la frase que evita la mayoría de las decepciones: EKS gestiona el plano de control, no el clúster.

Tienes las cuatro opciones de cómputo con sus límites reales —incluida la de que EKS sobre Fargate no admite DaemonSet, lo que obliga a mantener nodos para los complementos—, la creación del clúster con eksctl y con CDK, aws eks update-kubeconfig y la disciplina de mirar el contexto antes de escribir un delete. Y la autenticación moderna: entradas de acceso en lugar de aws-auth, con los conjuntos de permisos de 09-04 gobernando también el clúster, y Pod Identity como equivalente exacto del rol de tarea de ECS.

Tienes los manifiestos completos de la tienda: Deployment con las tres sondas bien separadas —startup para arrancar, readiness para recibir tráfico, liveness para reiniciar—, requests y limits como conceptos distintos, reparto por AZ con topologySpreadConstraints, preStop para no perder peticiones y contexto de seguridad sin privilegios; el Service; y el Ingress con el AWS Load Balancer Controller, que crea el mismo ALB del módulo 3 con el WAF del módulo 4, con target-type: ip y group.name para no pagar un balanceador por servicio. Más el autoescalado en dos planos: HPA para los pods, Karpenter para los nodos, y la observación de que para igualar tres parámetros de ECS hicieron falta cinco componentes.

Y tienes el argumento que casi nadie pone por escrito: el coste oculto de las actualizaciones. Tres versiones al año, 14 meses de soporte estándar, un salto automático de 73 a 438 USD mensuales al entrar en extendido, y 20-30 días de trabajo de plataforma al año revisando API retiradas, matrices de compatibilidad y drenajes de nodos. En ECS ese trabajo no existe. Con la comparativa honesta entre ambos —EKS compra flexibilidad, portabilidad y ecosistema, y lo paga con complejidad, personal y trabajo recurrente— y la decisión de MercadoFresco: se queda en ECS con Fargate, por un equipo de plataforma de dos personas, la ausencia de requisitos que ECS no cubra, la ausencia de necesidad multi-nube y la velocidad; con los seis criterios objetivos, escritos por adelantado, que cambiarían esa decisión el día que se cumplan.

Qué ha cambiado para MercadoFresco en este módulo. Ya no hay AMI que reconstruir cuando cambia una dependencia: hay una imagen en mercadofresco/tienda versionada por commit y desplegada por digest. Ya no hay sistema operativo que parchear: el anfitrión es de AWS y el escaneo continuo de Inspector avisa de lo que sí es tuyo. Y ya no hay arranques de dos minutos: hay tareas que reciben tráfico en cuarenta segundos, con un escalado programado que además las tiene listas antes de que llegue el pico del viernes.

graph TB
  U[Clientes] --> CF["CloudFront E2QWERTY123ABC<br/>+ waf-mercadofresco-cdn"]
  CF --> ALB["alb-mercadofresco-tienda<br/>tg-mercadofresco-tienda / -verde"]
  ALB --> FG["ECS Fargate<br/>svc-mercadofresco-tienda-fg<br/>arm64, 2-20 tareas"]
  FG --> AU[("aurora-mercadofresco-pedidos")]
  FG --> EC[("mercadofresco-catalogo<br/>ElastiCache")]
  FG --> DY[("mercadofresco-carritos")]
  FG --> SQS["cola-mercadofresco-pedidos"]
  SQS --> TR["ECS Fargate + Spot<br/>svc-mercadofresco-trabajadores"]
  TR --> SF["mercadofresco-procesar-pedido<br/>Lambdas de pago, stock y reparto"]
  ECR["ECR 555566667777<br/>mercadofresco/tienda"] -.digest.-> FG
  PIPE["pipeline-mercadofresco-tienda<br/>blue/green canario"] -.despliega.-> FG
  OBS["Container Insights, X-Ray<br/>mercadofresco-produccion"] -.observa.-> FG

Y aquí llega la pregunta que nadie ha hecho todavía y que el gerente hará el lunes por la mañana. La arquitectura está completa: es elástica, segura, observable, desplegable desde un pipeline, descrita en código y repartida en cinco cuentas. Pero ¿cuánto cuesta todo esto? ¿Está bien gastado? ¿Cómo se reparte entre producción, preproducción y desarrollo, y entre las cuatro ciudades? ¿Hay algo encendido que nadie usa? ¿Y qué se podría comprometer por adelantado para pagar menos?

En el módulo 11, «Mejores prácticas y gestión de costos», se responde con método. 11-01 revisa la arquitectura completa con el Well-Architected Framework y sus seis pilares, que es la forma estructurada de auditar lo construido. 11-02 pone orden en el etiquetado y la asignación de costes, convirtiendo las cinco etiquetas obligatorias en informes que alguien pueda leer. 11-03 analiza la factura de verdad con Cost Explorer. 11-04 pone límites y avisos con Budgets. 11-05 compromete capacidad con Savings Plans e instancias reservadas, que es donde la comparación Fargate frente a EC2 de 10-02 queda por fin cerrada. Y 11-06 cierra el curso con el proyecto final: diseñar, justificar y presupuestar una arquitectura completa para MercadoFresco, con todo lo aprendido en once módulos.

Curso de AWS

Módulo 1: Introducción a AWS

Módulo 2: Servicios principales de AWS

Módulo 3: Redes y entrega de contenido

Módulo 4: Seguridad e identidad

Módulo 5: Monitorización y gestión

Módulo 6: Bases de datos

Módulo 7: Integración de aplicaciones

Módulo 8: Herramientas para desarrolladores

Módulo 9: Infraestructura como código y gobierno de cuentas

Módulo 10: Contenedores en AWS

Módulo 11: Mejores prácticas y gestión de costos

© Copyright 2026. Todos los derechos reservados