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
- El modelo de responsabilidad compartida
- Comparación de EKS, AKS y GKE
- Identidad: IRSA y Workload Identity
- Nodos: grupos gestionados, plantillas e instancias interrumpibles
- Qué cambia en lo que ya sabes
- Actualización de versión de un clúster gestionado
- Coste real y palancas de ahorro
- Portabilidad y dependencia del proveedor
- La decisión para Rutas Norte
- Errores comunes y consejos
- Ejercicios
- Conclusión
- 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.
- 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í | Sí | 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 | Sí | Sí |
| 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 | Sí | Sí | No | Sí |
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"}'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 containerUn 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í | 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) | Sí |
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í | 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.
- 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 --approveLa 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/RutasNorteApiReservasEn 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).
- 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 |
Sí | Procesa una cola; si un pod muere, el mensaje vuelve a la cola |
informes-ocupacion |
Sí | 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 sí 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íasDiferencias 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) |
- 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: LocalCosas 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: Localconserva 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 | Sí | 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.
- 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 generalPor 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-reservastarda 45 segundos en estar listo y el drenaje va rápido, hay corte. - Tu
terminationGracePeriodSecondsy tu hookpreStop. - 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.
- 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 tocarAjustar 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 |
- 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 actualCambiar 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.
- La decisión para Rutas Norte
Los datos del caso
- Empresa pequeña: dos personas en
plataforma, seis endesarrollo, tres ensoporte. - 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ónDos 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íneaMigrar 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=ReleasedOrden 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-notificacioneseinformes-ocupacion, si se acompañan de tolerations, afinidad preferente, distribución topológica,preStopy 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
requestscon 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
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
