En 10-02 montamos un clúster con kubeadm y descubrimos todo lo que implica: preparar máquinas, configurar containerd, generar certificados que caducan al año, mantener el quórum de etcd, ensayar restauraciones y actualizar nodo a nodo. Terminamos con una advertencia clara: operar eso en producción exige un equipo dedicado con guardia.

En 10-05 montamos GitOps, y ahora todo el estado de la plataforma Rutas Norte vive en Git. Eso plantea una pregunta natural: si el clúster es reemplazable y se reconstruye solo, ¿por qué íbamos a mantenerlo nosotros?

Esta lección responde a esa pregunta. Veremos qué gestiona el proveedor y qué sigue siendo tuyo, compararemos EKS, AKS y GKE en los criterios que de verdad deciden, cerraremos la federación de identidad que dejamos pendiente en 03-06, aprovecharemos las instancias interrumpibles para las cargas tolerantes de Rutas Norte, y terminaremos con una recomendación razonada.

Contenido

  1. El modelo de responsabilidad compartida
  2. Comparación de EKS, AKS y GKE
  3. Identidad: IRSA y Workload Identity
  4. Nodos: grupos gestionados, plantillas e instancias interrumpibles
  5. Qué cambia en lo que ya sabes
  6. Actualización de versión de un clúster gestionado
  7. Coste real y palancas de ahorro
  8. Portabilidad y dependencia del proveedor
  9. La decisión para Rutas Norte
  10. Errores comunes y consejos
  11. Ejercicios
  12. Conclusión

  1. El modelo de responsabilidad compartida

Un clúster gestionado no es "Kubernetes sin trabajo". Es una frontera distinta entre lo que hace el proveedor y lo que haces tú.

flowchart TB
    subgraph prov["Responsabilidad del proveedor"]
        P1[apiserver, scheduler,<br/>controller-manager]
        P2[etcd: réplicas,<br/>copias, cifrado]
        P3[Certificados<br/>del plano de control]
        P4[Alta disponibilidad<br/>entre zonas]
        P5[Parches del plano<br/>de control]
    end
    subgraph tuyo["Responsabilidad tuya"]
        T1[Cargas de trabajo<br/>y sus recursos]
        T2[Nodos: tipo, tamaño,<br/>parches del SO]
        T3[Red: VPC, subredes,<br/>NetworkPolicies]
        T4[RBAC, PSS,<br/>secretos]
        T5[Coste]
        T6[Copias de los DATOS]
    end
    style prov fill:#e8ffe8
    style tuyo fill:#fff4e8

La tabla completa, contrastada con kubeadm

Responsabilidad kubeadm (10-02) Gestionado
Máquinas del plano de control Tuya Del proveedor
apiserver, scheduler, controller-manager Tuya Del proveedor
etcd: despliegue, quórum, copias Tuya (etcdctl snapshot) Del proveedor
Certificados del plano de control Tuya (kubeadm certs renew, caducan al año) Del proveedor
Alta disponibilidad del plano de control Tuya (HAProxy, 3 nodos) Del proveedor
Actualización del plano de control Tuya (kubeadm upgrade apply) Del proveedor (tú eliges cuándo)
Escalado del plano de control con la carga Tuya Del proveedor
Registro de auditoría del apiserver Tuya (configurar) Del proveedor (tú lo activas)
Sistema operativo de los nodos Tuya Tuya (imágenes del proveedor, tú aplicas)
Actualización del kubelet en los nodos Tuya Tuya (asistida)
CNI Tuya (instalar Calico) Del proveedor por defecto, cambiable por ti
Cargas de trabajo, sondas, recursos Tuya Tuya
RBAC, Pod Security Standards, políticas Tuya Tuya
NetworkPolicies Tuya Tuya
Secretos y su gestión Tuya Tuya
Copias de los datos de las aplicaciones Tuya (Velero, 05-06) Tuya (Velero)
Monitorización de las aplicaciones Tuya (Prometheus, 07-03) Tuya
Coste Tuya Tuya
Cumplimiento normativo y datos personales Tuya Tuya

Lo que hay que entender

El plano de control desaparece de tus preocupaciones. Los certificados que caducan al año, el quórum de etcd, los snapshots, el HAProxy delante de los tres nodos de control: todo eso se va. Es aproximadamente la mitad de la lección 10-02.

Todo lo demás sigue igual. Los nodos siguen siendo tuyos, con su sistema operativo que hay que parchear. El RBAC sigue siendo tuyo. Las NetworkPolicies siguen siendo tuyas. Y si postgres-reservas pierde datos, es tu problema, no el del proveedor.

Un error muy frecuente: "está gestionado, así que las copias están hechas". Falso. El proveedor respalda etcd (la definición de los objetos), no los volúmenes de tus aplicaciones. Los datos de clientes de postgres-reservas los respaldas tú con Velero o con los snapshots del proveedor. Lo vimos en 05-06 y sigue siendo así.

La regla de oro: un clúster gestionado te quita la mitad del trabajo de operación de Kubernetes. La otra mitad, la que has estudiado en los módulos 2 a 9, sigue siendo íntegramente tuya.

  1. Comparación de EKS, AKS y GKE

Coste del plano de control

EKS AKS GKE
Coste mensual del plano de control ~73 $ por clúster 0 $ en el nivel gratuito; ~73 $ con SLA ~73 $ por clúster (uno gratis por cuenta)
SLA incluido 99,95 % Solo con el nivel estándar 99,95 % (regional) / 99,5 % (zonal)
¿Aplica a cada clúster? Sí, salvo el primero

Con tres entornos separados en clústeres distintos, eso son unos 220 $/mes solo por existir. Es una de las razones por las que muchas empresas separan entornos por namespace dentro de un clúster (como hace Rutas Norte) en vez de por clúster.

Aviso: el nivel gratuito de AKS no tiene SLA financiero. Para rutas-norte-pro eso puede ser inaceptable, y entonces el coste se iguala.

Ciclo de versiones y ventana de soporte

EKS AKS GKE
Versiones menores soportadas ~4 simultáneas ~3 Depende del canal
Soporte estándar 14 meses 12 meses 14 meses (canal normal)
Soporte extendido (de pago) Sí, hasta 26 meses
Actualización automática Solo del parche Configurable Sí, por canales
Canales de versión No No Rápido, normal, estable

El sistema de canales de GKE es una diferencia real de operación: eliges "estable" y Google mantiene el clúster en una versión probada, actualizándolo en la ventana de mantenimiento que le indiques. Es lo más cerca que se está de no pensar en versiones.

En EKS y AKS, el ciclo es más manual. Y si dejas caducar la versión, ambos acaban forzando la actualización, lo que es peor que planificarla.

Modelo de red y asignación de IP a los pods

Esta es la diferencia técnica más importante y menos conocida, y la que provoca sorpresas serias en producción.

EKS (VPC CNI) AKS (Azure CNI) AKS (kubenet) GKE (nativo de VPC)
IP del pod Real de la VPC Real de la red virtual Superpuesta con NAT Real de la VPC (alias)
¿Consume IP de tu red? Sí, una por pod Sí, una por pod No Sí, de un rango secundario
Pods por nodo Limitado por el tipo de instancia Configurable, reserva por adelantado Alto Configurable
Rendimiento Nativo, sin superposición Nativo NAT, más latencia Nativo
Recursos externos alcanzan al pod directamente No

El problema que esto causa en la práctica: en EKS, cada pod consume una IP real de tu VPC. Una instancia t3.medium admite 3 interfaces de red con 6 IP cada una: 17 pods máximo, independientemente de que tenga CPU y memoria libres. Si tu subred es un /24 (254 direcciones), te quedas sin IP para pods mucho antes de quedarte sin CPU.

# En EKS: comprobar el límite real de pods de un nodo
kubectl get node ip-10-0-1-42.eu-west-1.compute.internal \
  -o jsonpath='{.status.allocatable.pods}{"\n"}'
17

Y el síntoma cuando se agota:

Warning  FailedCreatePodSandBox  2m  kubelet
  Failed to create pod sandbox: plugin type="aws-cni" failed:
  add cmd: failed to assign an IP address to container

Un pod Pending sin que falte CPU ni memoria. Desconcertante si no conoces el modelo.

Mitigaciones en EKS: activar el modo de prefijos delegados (ENABLE_PREFIX_DELEGATION), que multiplica por 16 las IP disponibles por interfaz; usar instancias más grandes; o planificar rangos de VPC generosos desde el principio (un /16, no un /24).

En AKS con Azure CNI clásico, hay que reservar por adelantado maxPods × número de nodos direcciones. Si planificas mal, no puedes escalar. La variante "superposición" (Azure CNI Overlay) resuelve esto y es la recomendada hoy.

En GKE con nativo de VPC, se usan rangos secundarios de IP alias, lo que separa el direccionamiento de pods del de nodos. Es el modelo más limpio de los tres.

Integración de identidad

EKS AKS GKE
Mecanismo IRSA (IAM Roles for Service Accounts) o Pod Identity Workload Identity (basado en Entra ID) Workload Identity Federation
Base Token OIDC proyectado en el pod Token OIDC Token OIDC
Madurez Muy alta (desde 2019) Alta Muy alta (el más antiguo)
Complejidad de configuración Media Media Baja

Los tres han convergido en el mismo mecanismo: OIDC. Lo detallamos en el apartado 3.

Grupos de nodos y modos automáticos

EKS AKS GKE
Grupos gestionados Sí (agrupaciones de nodos) Sí (agrupaciones)
Modo sin gestión de nodos Fargate (por pod) Nodos virtuales (ACI) Autopilot
Autoescalador de nodos Cluster Autoscaler o Karpenter Cluster Autoscaler o NAP Integrado o aprovisionamiento automático
Aprovisionamiento automático de tipos Con Karpenter Sí (NAP)

GKE Autopilot merece mención aparte: es un modo en el que no hay nodos en absoluto desde tu punto de vista. Declaras pods con sus requests y pagas exactamente por eso. No hay nodos que parchear ni que dimensionar, no hay DaemonSets propios, no hay hostPath ni privilegios. Es Kubernetes reducido a su interfaz de cargas de trabajo.

  • A favor: cero operación de nodos, facturación exacta por lo solicitado, seguridad reforzada por defecto.
  • En contra: restricciones fuertes (no puedes ejecutar Falco de 08-06 tal cual, ni ciertos DaemonSets), coste por unidad mayor si tus pods están mal dimensionados, menos control.

AWS Fargate es similar pero más limitado: no admite DaemonSets en absoluto, no admite almacenamiento persistente en bloque, ni privileged. Para worker-notificaciones puede encajar; para postgres-reservas, no.

Autoescalado

EKS AKS GKE
Cluster Autoscaler Lo instalas tú Integrado, se activa Integrado
Alternativa avanzada Karpenter (recomendada) NAP Aprovisionamiento automático
Velocidad de arranque de nodo 40-90 s (Karpenter: ~40 s) 60-120 s 40-80 s
Elección automática del tipo de instancia Karpenter sí NAP sí

Recordando 09-03: Karpenter no trabaja con grupos de nodos predefinidos, sino que elige el tipo de instancia óptimo para los pods pendientes. Si informes-ocupacion necesita 8 CPU una vez por noche, Karpenter levanta la instancia justa, la usa y la retira. Es una diferencia notable de coste y de velocidad.

Complementos gestionados y ecosistema

Complemento EKS AKS GKE
CNI Gestionado (VPC CNI) Gestionado Gestionado
CoreDNS, kube-proxy Gestionados Gestionados Gestionados
Controlador CSI Gestionado Gestionado Gestionado
Controlador de Ingress AWS Load Balancer Controller (lo instalas) Application Gateway (gestionado) GKE Ingress (nativo)
Métricas y registros CloudWatch Container Insights Azure Monitor Cloud Operations (el más integrado)
Malla de servicios App Mesh (en retirada) / Istio Istio gestionado Anthos Service Mesh
Escaneo de imágenes ECR scanning Defender for Containers Artifact Analysis
Política Ninguna nativa Azure Policy Policy Controller

Tabla resumen

Criterio EKS AKS GKE
Coste del plano de control Medio Bajo (gratis sin SLA) Medio (1 gratis)
Ventana de soporte Larga (14 m) Media (12 m) Larga (14 m)
Modelo de red IP real, límite por instancia Flexible, requiere planificación El más limpio
Identidad IRSA, muy maduro Workload Identity El más sencillo
Autoescalado Karpenter, el mejor Bueno Muy bueno
Modo sin nodos Fargate (limitado) Nodos virtuales Autopilot, el mejor
Facilidad de operación Media Media-alta Alta
Ecosistema y herramientas El mayor Bueno Muy bueno
Integración con el resto de la nube Muy buena Excelente si ya usas Microsoft Muy buena
Curva de aprendizaje Alta (IAM de AWS) Media Baja

Conclusión honesta: la mejor elección casi siempre es la nube donde ya está el resto de tu infraestructura. La diferencia técnica entre los tres es menor que el coste de operar en dos nubes a la vez. Si empiezas de cero y solo miras Kubernetes, GKE es el más pulido; si tu empresa ya vive en AWS, EKS con Karpenter es excelente; si tu empresa es un entorno Microsoft, AKS se integra sin fricción.

  1. Identidad: IRSA y Workload Identity

Cerramos aquí lo que quedó anunciado en 03-06.

El problema

api-reservas necesita leer un fichero de tarifas de un bucket de almacenamiento y publicar mensajes en una cola. La forma tradicional:

# LA FORMA MALA. No lo hagas.
apiVersion: v1
kind: Secret
metadata:
  name: credenciales-nube
stringData:
  ACCESO_ID: "AKIAIOSFODNN7EXAMPLE"
  ACCESO_SECRETO: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

Problemas: la credencial es permanente, hay que rotarla a mano, está en un Secret (base64, no cifrado, 03-02), y si se filtra sirve desde cualquier sitio del mundo.

La solución: federación OIDC

Los tres proveedores convergen en el mismo mecanismo. El clúster expone un emisor OIDC; el kubelet proyecta en el pod un token firmado de corta duración con la identidad de la ServiceAccount; el proveedor de nube confía en ese emisor y canjea el token por credenciales temporales.

sequenceDiagram
    participant P as Pod api-reservas
    participant K as kubelet
    participant O as Emisor OIDC<br/>del clúster
    participant I as IAM del proveedor
    participant S as Bucket / Cola

    K->>O: solicita token para la SA
    O-->>K: token JWT firmado (1 h)
    K->>P: lo proyecta en<br/>/var/run/secrets/.../token
    P->>I: "canjea este token por credenciales"
    I->>O: verifica la firma contra las claves públicas
    I-->>P: credenciales temporales (15 min - 1 h)
    P->>S: accede con esas credenciales

Ninguna credencial permanente. Nada que rotar. El token se renueva solo y solo sirve para esa ServiceAccount en ese clúster.

IRSA en EKS

# 1. Crear el proveedor de identidad OIDC del clúster (una sola vez)
eksctl utils associate-iam-oidc-provider --cluster rutas-norte-pro --approve

# 2. Crear el rol y asociarlo a la ServiceAccount
eksctl create iamserviceaccount --name api-reservas --namespace rutas-norte-pro \
  --cluster rutas-norte-pro \
  --attach-policy-arn arn:aws:iam::123456789012:policy/RutasNorteTarifasLectura --approve

La magia está en la política de confianza del rol, cuya condición limita quién puede asumirlo:

"Condition": { "StringEquals": {
  "oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLE...:sub":
    "system:serviceaccount:rutas-norte-pro:api-reservas",
  "oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLE...:aud": "sts.amazonaws.com"
}}

Fíjate en sub: solo la ServiceAccount api-reservas del namespace rutas-norte-pro puede asumir este rol. Ni otra SA, ni la misma SA en otro namespace.

# El manifiesto que va a Git: una anotación, ningún secreto
apiVersion: v1
kind: ServiceAccount
metadata:
  name: api-reservas
  namespace: rutas-norte-pro
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/RutasNorteApiReservas

En el Deployment basta con serviceAccountName: api-reservas: sin variables de credenciales, porque el SDK del proveedor detecta el token proyectado automáticamente. Puedes verificarlo con kubectl exec ... -- ls /var/run/secrets/eks.amazonaws.com/serviceaccount/.

Nota: AWS ha introducido EKS Pod Identity, más sencillo (una asociación en la API de EKS, sin proveedor OIDC ni anotación). Es la opción recomendada para clústeres nuevos; IRSA sigue siendo necesaria si la carga corre fuera de EKS.

Workload Identity en GKE y en AKS

El mecanismo es el mismo; cambian las órdenes y la anotación.

# --- GKE ---
gcloud container clusters update rutas-norte-pro \
  --workload-pool=rutas-norte-proyecto.svc.id.goog --region=europe-west1
gcloud iam service-accounts create rn-api-reservas
gcloud iam service-accounts add-iam-policy-binding \
  [email protected] \
  --role roles/iam.workloadIdentityUser \
  --member "serviceAccount:rutas-norte-proyecto.svc.id.goog[rutas-norte-pro/api-reservas]"

# --- AKS ---
az aks update -g rutas-norte -n rutas-norte-pro --enable-oidc-issuer --enable-workload-identity
EMISOR=$(az aks show -g rutas-norte -n rutas-norte-pro --query "oidcIssuerProfile.issuerUrl" -otsv)
az identity create -g rutas-norte -n rn-api-reservas
az identity federated-credential create --name rn-api-reservas-fed \
  --identity-name rn-api-reservas --resource-group rutas-norte --issuer "${EMISOR}" \
  --subject "system:serviceaccount:rutas-norte-pro:api-reservas"
EKS (IRSA) AKS GKE
Anotación en la SA eks.amazonaws.com/role-arn azure.workload.identity/client-id iam.gke.io/gcp-service-account
Requisito extra en el pod Ninguno Etiqueta azure.workload.identity/use: "true" Ninguno
Pasos de configuración 3 4 3
Duración de las credenciales 15 min - 12 h 1 h 1 h
Alternativa más simple EKS Pod Identity

Lo importante para ti: en los tres casos, el manifiesto que va a Git contiene una anotación con un identificador, nunca un secreto. Es exactamente el modelo que buscábamos en 03-06 y que encaja perfectamente con GitOps (10-05).

  1. Nodos: grupos gestionados, plantillas e instancias interrumpibles

Grupos de nodos gestionados

Un grupo de nodos gestionado es un conjunto de máquinas homogéneas que el proveedor mantiene: las crea, las une al clúster, las actualiza de forma gradual y las sustituye si fallan.

# Ejemplo con eksctl para Rutas Norte
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata: { name: rutas-norte-pro, region: eu-west-1, version: "1.30" }

managedNodeGroups:
  # Grupo general: cargas que sirven tráfico de clientes
  - name: general
    instanceType: m6i.large
    minSize: 3
    maxSize: 12
    desiredCapacity: 4
    availabilityZones: [eu-west-1a, eu-west-1b, eu-west-1c]  # reparto real (09-05)
    volumeSize: 60
    volumeType: gp3
    updateConfig: { maxUnavailablePercentage: 25 }

  # Grupo interrumpible: cargas tolerantes a interrupción
  - name: interrumpible
    instanceTypes: [m6i.large, m5.large, m5a.large, m6a.large]
    spot: true                        # <-- instancias interrumpibles
    minSize: 0
    maxSize: 20
    labels: { rutasnorte.example/interrumpible: "true" }
    taints:
      # Nada se planifica aquí salvo que lo tolere explícitamente
      - { key: rutasnorte.example/interrumpible, value: "true", effect: NoSchedule }

Fíjate en instanceTypes con cuatro tipos en el grupo interrumpible: cuantos más tipos aceptas, menor es la probabilidad de que el proveedor no tenga capacidad y te interrumpa todo a la vez.

Instancias interrumpibles (spot)

Son capacidad sobrante del proveedor a un 60-90 % de descuento, con una contrapartida: el proveedor puede reclamarla con un aviso de 2 minutos (AWS), 30 segundos (Azure) o 30 segundos (GCP).

Carga de Rutas Norte ¿Apta para spot? Por qué
tienda-web No Cara al cliente; una interrupción es una venta perdida
api-reservas No Igual, y además mantiene sesiones de reserva
postgres-reservas Rotundamente no Estado crítico; una interrupción durante una escritura es un riesgo
redis-cache Con cuidado Es caché, pero perderla de golpe provoca una avalancha sobre la base de datos
worker-notificaciones Procesa una cola; si un pod muere, el mensaje vuelve a la cola
informes-ocupacion CronJob nocturno; si falla, reintenta

Los manifiestos, con lo aprendido en 06-05 y 09-05 (el detalle completo está en la solución del ejercicio 1):

    spec:
      # 1. TOLERAR el taint del grupo interrumpible
      tolerations:
        - { key: rutasnorte.example/interrumpible, operator: Equal,
            value: "true", effect: NoSchedule }

      # 2. PREFERIR nodos interrumpibles, pero aceptar los normales si no
      #    hay capacidad (preferredDuringScheduling, NO required)
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - { key: rutasnorte.example/interrumpible, operator: In, values: ["true"] }

      # 3. REPARTIR entre zonas: una zona entera puede quedarse sin
      #    capacidad interrumpible de golpe
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels: { app: worker-notificaciones }

      # 4. Margen para terminar el mensaje en curso dentro del aviso
      terminationGracePeriodSeconds: 90
      containers:
        - name: worker
          lifecycle:
            preStop:
              exec: { command: ["/app/drenar", "--espera", "60s"] }

Y un PDB con minAvailable: 1, porque aunque sean interrumpibles, no queremos que caigan todos a la vez (09-05).

Matiz importante sobre el PDB: un PodDisruptionBudget protege frente a interrupciones voluntarias (drenaje de un nodo para actualizarlo). Una interrupción de una instancia spot es involuntaria: el proveedor se lleva la máquina, con PDB o sin él. El PDB ayuda porque los gestores de terminación (AWS Node Termination Handler, o el manejo nativo de Karpenter) cordonan y drenan el nodo al recibir el aviso, y ese drenaje respeta el PDB.

informes-ocupacion, al ser un CronJob nocturno (06-03), es un caso todavía más claro: además de la toleration puede llevar un nodeSelector duro sobre rutasnorte.example/interrumpible: "true" —si no hay capacidad, esperar es aceptable— y un backoffLimit: 6 generoso, porque la interrupción es esperable.

Karpenter: la alternativa moderna en EKS

En lugar de definir grupos con tipos fijos, declaras restricciones y Karpenter elige:

apiVersion: karpenter.sh/v1
kind: NodePool
metadata: { name: rutas-norte-lotes }
spec:
  template:
    spec:
      taints:
        - { key: rutasnorte.example/interrumpible, value: "true", effect: NoSchedule }
      requirements:
        - { key: karpenter.sh/capacity-type, operator: In, values: ["spot"] }
        - { key: kubernetes.io/arch, operator: In, values: ["amd64", "arm64"] }  # ARM es más barato
        - { key: karpenter.k8s.aws/instance-category, operator: In, values: ["c", "m", "r"] }
        - { key: topology.kubernetes.io/zone, operator: In,
            values: ["eu-west-1a", "eu-west-1b", "eu-west-1c"] }
      nodeClassRef: { group: karpenter.k8s.aws, kind: EC2NodeClass, name: predeterminado }
  limits: { cpu: "200", memory: 400Gi }
  disruption:
    # Consolidar: si los pods caben en menos nodos, migrarlos y
    # retirar los sobrantes. Ahorro directo.
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 60s
    expireAfter: 720h                    # renovar nodos cada 30 días

Diferencias con el Cluster Autoscaler (09-03):

Cluster Autoscaler Karpenter
Cómo decide Escala grupos predefinidos Elige la instancia óptima para los pods pendientes
Tipos de instancia Los del grupo Cientos, según las restricciones
Velocidad 60-120 s ~40 s
Consolidación Limitada Activa y agresiva
Interrupción de spot Con un manejador aparte Integrada
Disponibilidad Los tres proveedores Solo AWS (y Azure en desarrollo)

  1. Qué cambia en lo que ya sabes

Este apartado repasa las lecciones anteriores del curso y señala qué es distinto en la nube.

La StorageClass (05-04)

En minikube usábamos standard, un directorio del nodo. En la nube:

# EKS con EBS gp3
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: { name: rutas-norte-ssd }
provisioner: ebs.csi.aws.com
parameters: { type: gp3, iops: "6000", throughput: "250", encrypted: "true" }
allowVolumeExpansion: true
# CRÍTICO: el volumen no se crea hasta que el pod se planifica,
# para que se cree en la MISMA zona que el nodo.
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
Aspecto Local Nube
Modos de acceso Todo "funciona" con un nodo ReadWriteOnce de verdad: un disco de bloque solo se monta en una máquina
ReadWriteMany Con hostPath, sí Requiere sistema de ficheros compartido (EFS, Azure Files, Filestore), más caro y más lento
Zonas No existen Un disco pertenece a una zona. Si el pod se planifica en otra, no arranca
Expansión A veces no Sí, en caliente
Cifrado en reposo No Sí, con claves gestionadas
Coste 0 Por GB/mes y por IOPS

El error clásico: postgres-reservas con volumeBindingMode: Immediate crea el disco en eu-west-1a; el planificador coloca el pod en eu-west-1c; el pod se queda Pending para siempre con volume node affinity conflict. Solución: WaitForFirstConsumer, que es el valor que traen por defecto las StorageClass de los tres proveedores.

El Service de tipo LoadBalancer (04-02)

En local se quedaba en <pending> y hacía falta minikube tunnel. En la nube funciona de verdad:

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
  annotations:
    # Balanceador de red (capa 4), más rápido y barato que el de capa 7
    service.beta.kubernetes.io/aws-load-balancer-type: "external"
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
spec:
  type: LoadBalancer
  # Conservar la IP real del cliente. Sin esto, los registros y el
  # limitador de tasa ven la IP del nodo, no la del usuario.
  externalTrafficPolicy: Local

Cosas que aprender:

  • Cada Service de tipo LoadBalancer cuesta dinero (unos 20-25 $/mes más el tráfico). Por eso se usa uno solo para el controlador de Ingress y todo lo demás va por reglas de Ingress (04-04).
  • Las anotaciones son específicas de cada proveedor. Son la parte menos portable de tus manifiestos (apartado 8).
  • Borrar el Service borra el balanceador. Y si el balanceador tiene un registro DNS apuntando, se rompe.
  • externalTrafficPolicy: Local conserva la IP del cliente pero requiere que haya un pod en cada nodo con endpoints; si no, el tráfico a ese nodo se descarta.

Los tres proveedores ofrecen además un controlador de Ingress gestionado (AWS Load Balancer Controller, Application Gateway Ingress Controller, GKE Ingress) que crea un balanceador de capa 7 directamente desde el objeto Ingress. Es cómodo, pero te ata más al proveedor que ingress-nginx.

Registros de imágenes (08-05)

Proveedor Registro Escaneo Autenticación desde el clúster
AWS ECR Básico o mejorado (Inspector) Por rol del nodo o IRSA, sin imagePullSecrets
Azure ACR Defender for Containers Vinculación directa con AKS
GCP Artifact Registry Artifact Analysis Por cuenta de servicio del nodo

La ventaja práctica: desaparece el imagePullSecrets. El nodo se autentica con su identidad de nube. Un Secret menos que gestionar.

Y se integra con lo de 08-05: la firma con Cosign y la política de admisión funcionan igual, con la ventaja de que los tres registros ofrecen escaneo automático que alimenta lo aprendido en 08-06.

Métricas y registros (07-03, 07-05)

Prometheus propio Servicio del proveedor
Coste El del almacenamiento y los nodos Por métrica o por GB ingerido
Retención larga Requiere Thanos o Mimir Incluida
Consultas PromQL AWS y GCP sí (compatibles); Azure con KQL
Portabilidad Total Nula
Operación Tuya Del proveedor
Correlación con el resto de la nube Manual Nativa

Consejo pragmático: mantener Prometheus (07-03) para las métricas de las aplicaciones, porque tus alertas y paneles son portables, y usar el servicio del proveedor para los registros (07-05), porque operar EFK a escala es caro y doloroso, y el coste del servicio gestionado suele salir a cuenta. Es la combinación que elige la mayoría de equipos.

Cuidado con el coste de los registros: api-reservas con LOG_LEVEL=debug en producción puede generar cientos de GB al mes. Los tres proveedores cobran por ingesta.

  1. Actualización de versión de un clúster gestionado

Comparado con el procedimiento de kubeadm (10-02), esto es un paseo. Pero sigue exigiendo planificación, y quien lo trata como un botón se lleva un disgusto.

# EKS: plano de control y grupos de nodos, por separado
aws eks update-cluster-version --name rutas-norte-pro --kubernetes-version 1.31
aws eks update-nodegroup-version --cluster-name rutas-norte-pro --nodegroup-name general

# AKS
az aks upgrade -g rutas-norte -n rutas-norte-pro --kubernetes-version 1.31.1

# GKE
gcloud container clusters upgrade rutas-norte-pro --master --cluster-version 1.31.1
gcloud container clusters upgrade rutas-norte-pro --node-pool general

Por qué sigue exigiendo planificación

1. Las APIs eliminadas siguen siendo tu problema. Si un manifiesto de k8s/base usa una API que desaparece en 1.31, el clúster se actualiza y tu aplicación deja de desplegarse.

# Antes de actualizar, SIEMPRE
kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis

# Y escanear los manifiestos
kubent --context rutas-norte-pro
pluto detect-files -d k8s/

2. Los complementos de terceros tienen sus propias matrices de compatibilidad. cert-manager, ingress-nginx, KEDA, Prometheus Operator, el controlador CSI: cada uno soporta un rango de versiones. Una actualización del clúster sin actualizarlos puede romperlos.

Complemento Comprobar antes
cert-manager Matriz de compatibilidad del proyecto
ingress-nginx Tabla de versiones soportadas
KEDA Versión mínima de Kubernetes
Prometheus Operator Versión de las CRDs
Argo CD Versión soportada del apiserver
Controlador CSI del proveedor Versión del complemento gestionado

3. La actualización de los nodos reinicia todos los pods. El proveedor drena cada nodo por turnos. Eso ejercita:

  • Tus PodDisruptionBudgets (09-05): si están mal configurados, el drenaje se bloquea o, peor, se salta con --force.
  • Tus sondas de readiness (07-01): si api-reservas tarda 45 segundos en estar listo y el drenaje va rápido, hay corte.
  • Tu terminationGracePeriodSeconds y tu hook preStop.
  • Tu capacidad de absorber la pérdida temporal de un nodo: si el clúster está al 95 % de ocupación, drenar un nodo deja pods Pending.

4. El plano de control y los nodos se actualizan por separado, y hay que respetar el desfase de versiones (10-02): nodos como mucho 3 menores por debajo del apiserver, y nunca por encima.

Procedimiento recomendado para Rutas Norte

1. Leer las notas de la versión y las APIs eliminadas.
2. Ejecutar kubent/pluto sobre k8s/ y corregir lo que salga.
3. Revisar la matriz de compatibilidad de cada complemento;
   actualizar los que hagan falta ANTES.
4. Actualizar rutas-norte-dev. Esperar una semana de uso normal.
5. Actualizar rutas-norte-pre. Ejecutar las pruebas de carga con k6 (09-06).
6. Verificar que los PDB permiten el drenaje:
     kubectl get pdb -A -o wide
     kubectl drain <un-nodo> --dry-run=server
7. Ventana de mantenimiento en rutas-norte-pro:
     a. Plano de control primero.
     b. Verificar que todo sigue Synced y Healthy en Argo CD.
     c. Un grupo de nodos cada vez, con maxUnavailable bajo.
     d. Vigilar los paneles de Grafana durante el proceso.
8. Actualizar la versión declarada en el script local de 10-01
   para que el equipo desarrolle contra la misma versión.

Ese último punto cierra el círculo con la primera lección del módulo: el entorno local debe seguir a producción.

  1. Coste real y palancas de ahorro

De qué se compone la factura

Concepto Peso típico Comentario
Nodos (cómputo) 50-70 % La partida grande, casi siempre
Almacenamiento 10-20 % Discos de los PV, snapshots, registros
Tráfico de red 5-20 % Salida a internet y entre zonas
Balanceadores 5-10 % ~20-25 $/mes cada uno
Plano de control 2-5 % ~73 $/mes por clúster
Servicios gestionados Variable Métricas, registros, escaneo

Una sorpresa habitual: el tráfico entre zonas de disponibilidad se cobra. Si api-reservas está en la zona A y postgres-reservas en la zona B, cada consulta cruza una frontera facturada. Con volumen alto, eso son cientos de euros al mes. Es un argumento a favor de la afinidad de zona... que choca con la alta disponibilidad de 09-05. Es un compromiso real que hay que decidir conscientemente.

Las palancas, por orden de rentabilidad

1. Dimensionar bien (lo aprendido en 09-02). Es la palanca con mejor relación esfuerzo/ahorro, y casi nadie la usa. El patrón típico: alguien puso requests: {cpu: 1, memory: 2Gi} "por si acaso" y el pod usa 80m y 200Mi. El planificador reserva lo pedido, así que estás pagando 12 veces lo que usas.

kubectl top pods -n rutas-norte-pro --sort-by=memory
kubectl describe vpa api-reservas -n rutas-norte-pro   # VPA en modo Off: recomienda sin tocar
  Container Recommendations:
    Container Name:  api
    Target:  Cpu: 127m   Memory: 312Mi

Ajustar requests a la recomendación en toda la plataforma suele reducir la factura de cómputo entre un 30 y un 50 %.

2. Autoescalado real (09-01 y 09-03). Con el HPA ajustando pods y Karpenter o el Cluster Autoscaler ajustando nodos, pagas por la carga real y no por el pico anual. Rutas Norte tiene picos en los puentes: sin autoescalado, o dimensionas para el puente todo el año, o te caes en el puente.

3. Instancias interrumpibles. 60-90 % de descuento en worker-notificaciones e informes-ocupacion.

4. Apagar los entornos que no se usan de noche. rutas-norte-dev no hace nada de las 20:00 a las 8:00 ni los fines de semana. Eso son 128 de 168 horas semanales: un 76 % de ahorro en ese entorno.

# CronJob que escala a cero por la noche (06-03). El de encendido es
# idéntico con schedule "0 8 * * 1-5" y --replicas=1.
apiVersion: batch/v1
kind: CronJob
metadata: { name: apagar-dev, namespace: rutas-norte-dev }
spec:
  schedule: "0 20 * * 1-5"
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: escalador-entorno
          restartPolicy: OnFailure
          containers:
            - name: kubectl
              image: bitnami/kubectl:1.30.4
              command: ["sh", "-c", "kubectl scale deploy,statefulset --all --replicas=0 -n rutas-norte-dev"]

Atención con GitOps: si Argo CD tiene selfHeal activo en rutas-norte-dev, revertirá el escalado a cero en tres minutos. Por eso en la solución del ejercicio 1 de 10-05 dejamos selfHeal: false en dev. La alternativa limpia es escalar el grupo de nodos en vez de los Deployments, o usar una ventana de sincronización del AppProject.

5. Compromisos de uso. Los tres proveedores descuentan un 30-55 % a cambio de comprometer un gasto o una capacidad durante 1 o 3 años (Savings Plans y Reserved Instances en AWS, Reservations en Azure, Committed Use Discounts en GCP).

Estrategia recomendada: comprometer solo la base estable (los nodos que están encendidos siempre), dejar el pico en bajo demanda y las cargas tolerantes en spot.

flowchart TB
    A["Carga total"] --> B["Base estable<br/>compromiso 1-3 años<br/>-40%"]
    A --> C["Picos<br/>bajo demanda<br/>precio completo"]
    A --> D["Cargas tolerantes<br/>interrumpibles<br/>-70%"]
    style B fill:#e8ffe8
    style D fill:#e8f4ff

6. Visibilidad del coste por equipo. Herramientas como OpenCost o Kubecost reparten el gasto por namespace, etiqueta o equipo. Sin eso, nadie sabe que informes-ocupacion cuesta 400 €/mes y nadie tiene incentivo para arreglarlo. Las etiquetas de 02-07 (app.kubernetes.io/part-of, entorno) son la base de ese reparto.

Resumen de palancas

Palanca Ahorro típico Esfuerzo Riesgo
Ajustar requests con el VPA 30-50 % del cómputo Bajo Bajo si se hace por fases
Autoescalado de pods y nodos 20-40 % Medio Bajo
Instancias interrumpibles 60-90 % de las cargas aptas Medio Medio (hay que preparar la app)
Apagar entornos de noche 60-75 % de dev/pre Bajo Muy bajo
Compromisos de uso 30-55 % de la base Bajo Medio (compromiso a años)
Reducir tráfico entre zonas 5-15 % Alto Choca con la HA
Revisar retención de registros 10-30 % de observabilidad Bajo Bajo

  1. Portabilidad y dependencia del proveedor

Todo esto vale de poco si dentro de dos años quieres cambiar de nube y no puedes. Vamos a ser precisos sobre qué es portable en Rutas Norte.

Qué es portable

Elemento Portable Comentario
Deployment, StatefulSet, DaemonSet, Job, CronJob Total API estándar
Service, Ingress (reglas) Total Las reglas sí; las anotaciones no
ConfigMap, Secret Total
RBAC, ServiceAccount Total Salvo las anotaciones de identidad
HPA, PDB, topologySpreadConstraints Total
NetworkPolicy Total Si el CNI destino las implementa
PVC (nombre de la clase) Alta El PVC es portable; la StorageClass hay que redefinirla
Manifiestos de Kustomize Total
Charts de Helm Total
Applications de Argo CD Total
Paneles y alertas de Prometheus Total Si usas Prometheus y no el servicio del proveedor

Qué NO es portable

Elemento Por qué Coste de migrar
Anotaciones de Service LoadBalancer service.beta.kubernetes.io/aws-* no existe en Azure Bajo: reescribir un bloque
parameters de la StorageClass ebs.csi.aws.com frente a disk.csi.azure.com Bajo: un fichero
Anotación de identidad en la SA IRSA / Workload Identity Bajo: una anotación
IngressClass del proveedor ALB, Application Gateway, GKE Ingress Medio
Servicios de datos gestionados (RDS, Cloud SQL) Fuera de Kubernetes Alto
Colas y mensajería gestionadas (SQS, Service Bus) Fuera de Kubernetes Alto
Métricas y registros del proveedor Consultas y paneles propietarios Alto
Infraestructura como código específica CloudFormation, ARM, Deployment Manager Medio (Terraform mitiga)

El patrón evidente: lo que está dentro de Kubernetes es portable; lo que está fuera no lo es. La dependencia del proveedor no viene de EKS, AKS o GKE, sino de los servicios que rodean al clúster.

Cómo minimizar la dependencia

1. Aísla lo no portable en las superposiciones de Kustomize. Ya lo tienes montado desde 10-04:

k8s/
├── base/                      <- 100 % portable
├── componentes/
│   ├── proveedor-aws/         <- StorageClass, anotaciones IRSA, ALB
│   ├── proveedor-azure/
│   └── proveedor-gcp/
└── entornos/pro/
    └── kustomization.yaml     <- activa el componente del proveedor actual

Cambiar de nube es cambiar una línea en components:. La base no se toca.

2. Usa ingress-nginx en vez del controlador del proveedor. Es una capa de abstracción que cuesta algo de rendimiento y ahorra una migración entera. Las reglas de Ingress y las anotaciones de nginx funcionan igual en las tres nubes.

3. Usa Prometheus para las métricas. Tus alertas y paneles son el conocimiento operativo acumulado de años. En PromQL son portables; en el lenguaje de consulta del proveedor, no.

4. Terraform en vez de la herramienta nativa. No hace la infraestructura portable, pero sí el modelo mental y los flujos de trabajo.

5. Decide conscientemente sobre las bases de datos. Ejecutar postgres-reservas como StatefulSet con un operador (06-01, 06-07) es portable; usar el servicio gestionado del proveedor es más cómodo y menos portable. Ambas son decisiones defendibles; lo que no vale es tomarla sin darse cuenta.

PostgreSQL en Kubernetes Servicio gestionado
Portabilidad Alta Baja
Operación (copias, réplicas, parches) Tuya, con el operador Del proveedor
Coste Menor Mayor
Rendimiento a gran escala Requiere ajuste Optimizado
Riesgo de pérdida de datos Depende de tu diligencia Menor

6. Sé realista. La portabilidad total cuesta dinero y complejidad, y muchas empresas nunca cambian de nube. Un objetivo sensato: "podríamos migrar en tres meses con esfuerzo, no en tres días". Eso te da poder de negociación y te protege de un cambio de precios abusivo, sin pagar el impuesto de la abstracción total.

  1. La decisión para Rutas Norte

Los datos del caso

  • Empresa pequeña: dos personas en plataforma, seis en desarrollo, tres en soporte.
  • Ninguna cobertura de guardia 24×7.
  • Tráfico con picos fuertes y previsibles: puentes y vacaciones, 8-10 veces la carga base.
  • Datos personales de clientes en postgres-reservas: exigencias del RGPD sobre localización y cifrado.
  • Presupuesto ajustado.
  • Ya usa almacenamiento de objetos y correo transaccional de AWS para otras cosas.

Descartamos el clúster autogestionado

Con dos personas en plataforma y sin guardia, kubeadm (10-02) queda descartado. La pregunta decisiva es: ¿quién restaura etcd un domingo a las 4 de la mañana? Si la respuesta es "esperamos al lunes", no se puede operar rutas-norte-pro con un clúster propio. El ahorro del plano de control (unos 220 €/mes con tres clústeres) no compensa ni de lejos el riesgo.

La elección: EKS

Criterio Peso Valoración
Ya usan AWS para otros servicios Alto Una sola nube, una sola facturación, una sola identidad
Karpenter para los picos de los puentes Alto El mejor autoescalado de nodos de los tres
Spot para worker-notificaciones e informes-ocupacion Alto Ahorro directo sobre cargas ya identificadas como tolerantes
IRSA muy maduro Medio Cierra 03-06 sin credenciales en Secrets
Regiones europeas (RGPD) Alto eu-west-1 cumple
Ventana de soporte de 14 meses Medio Con un equipo pequeño, más tiempo entre actualizaciones es valioso
Coste del plano de control Bajo 73 $/mes es asumible
Curva de aprendizaje de IAM Medio (en contra) Es el punto flojo, pero el equipo ya lo conoce

Si Rutas Norte no usara ya AWS, la recomendación sería GKE: el modelo de red más limpio, Workload Identity más sencilla, canales de versión que reducen el trabajo de actualización, y Autopilot como opción para los entornos no productivos.

El diseño concreto

Cuenta de AWS, región eu-west-1

CLÚSTER 1: rutas-norte-pro
  Kubernetes 1.30, plano de control regional (3 zonas)
  Grupos de nodos:
    - general:       m6i.large, 3-12 nodos, bajo demanda,
                     3 zonas, con compromiso de uso sobre 3 nodos
    - interrumpible: gestionado por Karpenter, spot, 0-20,
                     con taint rutasnorte.example/interrumpible
  Cargas:
    tienda-web, api-reservas, postgres-reservas, redis-cache -> general
    worker-notificaciones, informes-ocupacion -> interrumpible

CLÚSTER 2: rutas-norte-no-productivo
  Namespaces rutas-norte-dev y rutas-norte-pre
  Grupo único: m6i.large, 2-6 nodos, MAYORÍA SPOT
  Apagado automático de 20:00 a 8:00 y fines de semana

PLATAFORMA (en ambos clústeres, por GitOps):
  Argo CD, cert-manager, ingress-nginx, KEDA,
  kube-prometheus-stack, External Secrets Operator, Velero

DATOS:
  postgres-reservas como StatefulSet con operador (portabilidad)
  Copias con Velero + snapshots de EBS, replicadas a otra región

Dos clústeres, no tres. Separar pro del resto es una frontera de seguridad real; separar dev de pre no lo justifica: los namespaces con cuotas (03-04) y NetworkPolicies (04-06) son suficientes, y ahorran 73 $/mes más la operación.

El coste estimado

Concepto Mensual
Plano de control × 2 clústeres 146 $
Nodos de pro: 3 bajo demanda con compromiso + 1 medio ~210 $
Nodos interrumpibles de pro (media 2) ~35 $
Nodos no productivos (spot, 40 h/semana) ~45 $
Almacenamiento EBS (300 GB gp3 + snapshots) ~45 $
Balanceadores (1 por clúster) ~50 $
Tráfico de salida y entre zonas ~60 $
Registro y métricas gestionadas ~40 $
Total aproximado ~630 $/mes

Sin las palancas de ahorro (todo bajo demanda, sin spot, sin apagado nocturno, sin compromisos, con requests sobredimensionadas) la misma plataforma costaría del orden de 1.400-1.600 $/mes. Las palancas del apartado 7 no son teoría: son más de la mitad de la factura.

La conclusión honesta

Un clúster gestionado no es más barato que uno propio en coste de infraestructura pura. Es más barato en coste total, porque incluye el trabajo que no tienes que hacer y el riesgo que no asumes. Para Rutas Norte S.L., con dos personas en plataforma y sin guardia, es la única opción defendible.

Y hay una simetría que conviene notar: gracias a GitOps (10-05), la decisión es reversible. Todo el estado de la plataforma está en Git. Si mañana hay una razón para cambiar de nube o para montar un clúster propio, se levanta el clúster nuevo, se instala Argo CD, se aplica la Application raíz y en veinte minutos la plataforma está de pie. Los datos se migran aparte, que es la parte difícil, pero la plataforma se reconstruye sola.

Errores Comunes y Consejos

1. Creer que "gestionado" incluye las copias de tus datos. El proveedor respalda etcd, no tus volúmenes. postgres-reservas es tu responsabilidad: Velero (05-06) y snapshots, con restauración ensayada.

2. No planificar el direccionamiento IP. En EKS con VPC CNI, cada pod consume una IP real de la VPC. Un /24 se agota enseguida y el síntoma (FailedCreatePodSandBox) no menciona las IP de forma obvia. Planifica rangos generosos y activa la delegación de prefijos.

3. Un Service de tipo LoadBalancer por aplicación. Cada uno cuesta 20-25 $/mes. Usa uno para el controlador de Ingress y enruta con reglas de Ingress (04-04).

4. StorageClass con volumeBindingMode: Immediate en un clúster multizona. El disco se crea en una zona, el pod se planifica en otra, y el pod nunca arranca. Usa WaitForFirstConsumer.

5. Poner cargas con estado en instancias interrumpibles. postgres-reservas en spot es una receta para perder datos. Solo cargas que toleren morir sin aviso, con terminationGracePeriodSeconds y hook preStop adecuados.

6. Un solo tipo de instancia en el grupo interrumpible. Si el proveedor se queda sin ese tipo, se lleva todos tus nodos a la vez. Declara 4-6 tipos equivalentes.

7. Seguir usando credenciales estáticas en Secrets. IRSA, Workload Identity y su equivalente en Azure eliminan las credenciales permanentes. Es de las mejoras de seguridad con mejor relación coste/beneficio que existen.

8. Actualizar la versión sin comprobar las APIs eliminadas ni los complementos. El clúster se actualiza sin problema y tu aplicación deja de desplegarse, o ingress-nginx deja de arrancar. kubent y las matrices de compatibilidad, antes de tocar nada.

9. requests sobredimensionadas "por si acaso". Es la principal causa de sobrecoste en Kubernetes. El VPA en modo Off (09-02) te da los números reales en una semana.

10. Ignorar el tráfico entre zonas. En aplicaciones muy habladoras puede ser una partida importante. Mídelo antes de decidir; es un compromiso real con la alta disponibilidad de 09-05.

11. No etiquetar para el reparto de costes. Sin entorno, app.kubernetes.io/part-of y una etiqueta de equipo, la factura es un número global que nadie puede reducir. Las etiquetas de 02-07 son la base de OpenCost o Kubecost.

12. Adoptar Autopilot o Fargate sin comprobar las restricciones. Falco (08-06), algunos DaemonSets de red y todo lo que necesite privilegios pueden no funcionar. Prueba primero en un entorno no productivo.

13. Confundir "menos operación" con "sin operación". Sigues siendo responsable de los nodos, del RBAC, de las políticas, de los secretos, de la observabilidad y del coste. Los módulos 2 a 9 de este curso siguen siendo enteramente aplicables.

Ejercicios

Ejercicio 1: manifiestos para instancias interrumpibles

Escribe el conjunto completo de manifiestos que permite ejecutar worker-notificaciones en un grupo de nodos interrumpibles marcado con el taint rutasnorte.example/interrumpible=true:NoSchedule, sin dejar de funcionar si no hay capacidad spot. Debe incluir: tolerations, afinidad de nodo preferente, distribución topológica, terminationGracePeriodSeconds con hook preStop, un PDB y un ScaledObject de KEDA. Explica por qué usas afinidad preferente y no nodeSelector, y qué protege realmente el PDB frente a una interrupción de spot.

Ejercicio 2: componente de proveedor con Kustomize

Diseña el componente k8s/componentes/proveedor-aws/ que contenga todo lo específico de AWS que Rutas Norte necesita: la StorageClass rutas-norte-ssd con EBS gp3 cifrado, un parche que añada la anotación de IRSA a las ServiceAccounts de api-reservas y worker-notificaciones, y un parche que añada las anotaciones de balanceador de red al Service del controlador de Ingress. Explica cómo se activaría desde la superposición de pro y qué habría que cambiar para migrar a Azure.

Ejercicio 3: plan de reducción de costes

La factura de rutas-norte-pro es de 1.450 $/mes: 980 $ de cómputo, 180 $ de almacenamiento, 120 $ de balanceadores (5 Services de tipo LoadBalancer), 90 $ de tráfico y 80 $ de registros. Todos los pods tienen requests: {cpu: 500m, memory: 1Gi} y kubectl top muestra un uso medio de 90m y 220Mi. No hay instancias interrumpibles ni compromisos. Propón un plan priorizado con las órdenes de diagnóstico, el ahorro estimado de cada medida y el riesgo asociado.

Soluciones

Solución 1

apiVersion: apps/v1
kind: Deployment
metadata:
  name: worker-notificaciones
  namespace: rutas-norte-pro
spec:
  selector:
    matchLabels: { app: worker-notificaciones }
  template:
    metadata:
      labels:
        app: worker-notificaciones
        app.kubernetes.io/part-of: rutas-norte
    spec:
      tolerations:
        - key: rutasnorte.example/interrumpible
          operator: Equal
          value: "true"
          effect: NoSchedule
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - { key: rutasnorte.example/interrumpible, operator: In, values: ["true"] }
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels: { app: worker-notificaciones }
      terminationGracePeriodSeconds: 90
      containers:
        - name: worker
          image: registry.rutasnorte.example/worker-notificaciones:1.8.4
          lifecycle:
            preStop:
              exec: { command: ["/app/drenar", "--espera", "60s"] }
          resources:
            requests: { cpu: 200m, memory: 256Mi }
            limits:   { memory: 512Mi }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: worker-notificaciones, namespace: rutas-norte-pro }
spec:
  minAvailable: 1
  selector:
    matchLabels: { app: worker-notificaciones }
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: { name: worker-notificaciones, namespace: rutas-norte-pro }
spec:
  scaleTargetRef: { name: worker-notificaciones }
  minReplicaCount: 1
  maxReplicaCount: 30
  triggers:
    - type: redis
      metadata:
        address: redis-cache:6379
        listName: cola-correos
        listLength: "20"

Afinidad preferente y no nodeSelector: nodeSelector es una restricción dura. Si no hay capacidad spot en la región —cosa que ocurre precisamente en los picos, cuando más falta hace— los pods se quedarían Pending indefinidamente. Con preferredDuringScheduling, el planificador intenta el nodo interrumpible y, si no hay, usa uno bajo demanda: se paga más, pero el servicio no se detiene.

Qué protege el PDB: nada frente a la interrupción en sí (el proveedor se lleva la máquina). Protege durante el drenaje que el manejador de terminación (o Karpenter) ejecuta al recibir el aviso de 2 minutos: ese drenaje respeta el PDB, así que no se vacían dos nodos simultáneamente dejando la cola sin consumidores.

Solución 2

# k8s/componentes/proveedor-aws/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
resources:
  - storageclass.yaml
patches:
  - path: irsa-patch.yaml
    target: { kind: ServiceAccount, name: "api-reservas|worker-notificaciones" }
  - path: lb-patch.yaml
    target: { kind: Service, name: ingress-nginx-controller }

storageclass.yaml es el del apartado 5 (provisioner ebs.csi.aws.com, gp3 cifrado, WaitForFirstConsumer). Los dos parches son fusiones estratégicas mínimas; el target se encarga de a quién se aplican, así que el metadata.name del parche es irrelevante:

# irsa-patch.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: IGNORADO
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/RutasNortePro
---
# lb-patch.yaml
apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx-controller
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "external"
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
spec:
  externalTrafficPolicy: Local
# k8s/entornos/pro/kustomization.yaml
components:
  - ../../componentes/politicas-red
  - ../../componentes/alta-disponibilidad
  - ../../componentes/proveedor-aws     # <-- una línea

Migrar a Azure: crear k8s/componentes/proveedor-azure/ con provisioner: disk.csi.azure.com, la anotación azure.workload.identity/client-id (más la etiqueta azure.workload.identity/use: "true" en los pods) y las anotaciones de balanceador de Azure. Después, cambiar esa única línea. k8s/base/ no se toca en absoluto.

Solución 3

Prioridad Medida Diagnóstico Ahorro Riesgo
1 Ajustar requests a 150m/320Mi kubectl top pods, VPA en modo Off ~450 $ Bajo, por fases
2 Consolidar 5 balanceadores en 1 + Ingress kubectl get svc -A --field-selector spec.type=LoadBalancer ~96 $ Medio (cambio de DNS)
3 Spot para worker e informes Ya identificadas como tolerantes ~80 $ Medio
4 Compromiso de uso sobre la base Media de nodos de 90 días ~130 $ Medio (1-3 años)
5 Retención de registros de 30 a 7 días; LOG_LEVEL=warn Volumen ingerido por servicio ~50 $ Bajo
6 Revisar PV huérfanos y snapshots viejos kubectl get pv --field-selector status.phase=Released ~40 $ Bajo
Total ~846 $ Factura de ~1.450 a ~604 $
# Órdenes de diagnóstico
kubectl top pods -A --sort-by=cpu
kubectl get vpa -A -o custom-columns='NS:.metadata.namespace,NOMBRE:.metadata.name,CPU:.status.recommendation.containerRecommendations[0].target.cpu,MEM:.status.recommendation.containerRecommendations[0].target.memory'
kubectl get svc -A --field-selector spec.type=LoadBalancer
kubectl get pv --field-selector status.phase=Released

Orden y riesgo: se empieza por el ajuste de requests porque es el de mayor ahorro y menor riesgo, aplicándolo primero en pre, verificando con k6 (09-06) y luego en pro componente a componente. El compromiso de uso va el último a propósito: solo tiene sentido comprometerse a una capacidad después de haberla reducido, o estarías pagando por adelantado el sobredimensionamiento.

Conclusión

Cierra aquí el módulo 10. Lo esencial de esta lección:

  • El modelo de responsabilidad compartida te quita el plano de control entero —etcd, certificados, quórum, actualizaciones, alta disponibilidad—, es decir, aproximadamente la mitad del trabajo de 10-02. Todo lo demás, incluidos los nodos, el RBAC, las políticas, los secretos, la observabilidad, las copias de tus datos y el coste, sigue siendo tuyo.
  • EKS, AKS y GKE son más parecidos que distintos. Las diferencias que de verdad deciden son el modelo de red y la asignación de IP a los pods, el ciclo de versiones, el autoescalado (Karpenter destaca) y los modos automáticos (Autopilot destaca). Y por encima de todo: la nube donde ya está tu infraestructura.
  • La federación de identidad —IRSA, Workload Identity y su equivalente en Azure— elimina las credenciales permanentes de los Secrets. El manifiesto que va a Git lleva una anotación, no un secreto. Cierra lo prometido en 03-06.
  • Las instancias interrumpibles son ahorro real para worker-notificaciones e informes-ocupacion, si se acompañan de tolerations, afinidad preferente, distribución topológica, preStop y PDB.
  • Lo que ya sabías cambia de forma concreta: la StorageClass es del proveedor y necesita WaitForFirstConsumer, el LoadBalancer por fin obtiene IP (y factura), el registro se autentica con la identidad del nodo, y las métricas y registros se pueden delegar.
  • La actualización sigue exigiendo planificación: APIs eliminadas, matrices de compatibilidad de los complementos, PDB que permitan drenar y sondas que aguanten el reinicio.
  • El coste se domina con seis palancas, y las dos primeras —ajustar requests con el VPA y apagar lo que no se usa— son las de mejor relación esfuerzo/beneficio.
  • Y la portabilidad depende de lo que pongas fuera de Kubernetes, no de qué gestionado elijas. Aislar lo específico del proveedor en un componente de Kustomize deja la base 100 % portable.

Para Rutas Norte S.L., la recomendación es EKS en eu-west-1, con dos clústeres, Karpenter, instancias interrumpibles para las cargas tolerantes y las palancas de ahorro aplicadas desde el primer día. Y gracias a GitOps, esa decisión es reversible.

Con esto termina el recorrido por el ecosistema. Ya tienes todas las piezas: clústeres locales y propios, empaquetado con Helm, superposiciones con Kustomize, despliegue continuo con GitOps y una plataforma gestionada donde apoyarlo todo. Sumadas a los nueve módulos anteriores —pods, servicios, configuración, red, almacenamiento, patrones avanzados, observabilidad, seguridad y escalado—, ya conoces todas las piezas por separado.

Lo que falta es verlas funcionar juntas. En el Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real recorreremos escenarios completos de principio a fin: el despliegue de una aplicación web desde cero, la ejecución de aplicaciones con estado, una canalización de CI/CD completa, las estrategias de despliegue blue-green y canary, la gestión multi-clúster y, para cerrar, la operación en producción de verdad: incidencias, runbooks y control de costes. Ahí es donde el conocimiento se convierte en criterio.

Curso de Kubernetes

Módulo 1: Introducción a Kubernetes

Módulo 2: Componentes Principales de Kubernetes

Módulo 3: Gestión de Configuración y Secretos

Módulo 4: Redes en Kubernetes

Módulo 5: Almacenamiento en Kubernetes

Módulo 6: Conceptos Avanzados de Kubernetes

Módulo 7: Monitoreo y Registro

Módulo 8: Seguridad en Kubernetes

Módulo 9: Escalado y Rendimiento

Módulo 10: Ecosistema y Herramientas de Kubernetes

Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real

Módulo 12: Preparación para la Certificación de Kubernetes

© Copyright 2026. Todos los derechos reservados