Al final de la lección anterior quedaron tres preguntas abiertas. ¿Cómo hacer que la migración de esquema se ejecute automáticamente antes de que arranque api-reservas, en lugar de como un Job aparte? ¿Cómo evitar que api-reservas entre en CrashLoopBackOff cuando arranca antes que postgres-reservas? ¿Cómo exportar métricas de PostgreSQL sin tocar la imagen oficial?
Las tres se responden igual: poniendo más de un contenedor en el mismo pod.
Desde la lección 02-01 sabemos que un pod es un grupo de contenedores que comparten espacio de nombres de red —y por tanto localhost—, volúmenes y ciclo de vida. Hasta ahora no habíamos aprovechado esa capacidad: todos los pods de Rutas Norte tienen un solo contenedor. En esta lección la usaremos con criterio, que es la parte difícil: la mayoría de pods multi-contenedor mal diseñados nacen de meter dos aplicaciones juntas porque "van relacionadas", y eso siempre acaba mal.
Contenido
- Cuándo un pod necesita varios contenedores y cuándo eso es un error
- Init containers: preparación secuencial antes del arranque
- Casos de uso de init containers en Rutas Norte
- Diagnóstico de init containers fallidos
- Sidecars nativos: init containers con
restartPolicy: Always - Los tres patrones clásicos: sidecar, embajador y adaptador
- Patrón sidecar: exportador de métricas junto a
postgres-reservas - Patrón embajador: la conexión con la pasarela de pagos
- Patrón adaptador: normalizar el log de
worker-notificaciones - Orden de arranque y terminación, y el coste real
- Cuándo un pod necesita varios contenedores y cuándo eso es un error
La regla de oro, formulada con precisión:
Dos procesos van en el mismo pod cuando están tan acoplados que no tiene sentido escalarlos, actualizarlos ni ejecutarlos por separado, y uno de ellos existe únicamente para servir al otro.
La consecuencia inmediata de compartir pod es que comparten destino: se programan en el mismo nodo, escalan juntos, se reinician juntos y se actualizan juntos. Si api-reservas necesita 5 réplicas y worker-notificaciones necesita 2, ponerlos en el mismo pod obliga a tener 5 de cada uno. Eso es un error de diseño, no una optimización.
| Situación | ¿Mismo pod? | Por qué |
|---|---|---|
api-reservas + exportador de sus métricas |
Sí | El exportador no sirve para nada sin su aplicación; escalan a la vez |
api-reservas + worker-notificaciones |
No | Escalan distinto, se despliegan distinto, son dos aplicaciones |
worker-notificaciones + adaptador de logs |
Sí | El adaptador procesa el fichero local de ese proceso concreto |
tienda-web + postgres-reservas |
No | Ciclos de vida y necesidades de almacenamiento radicalmente distintos |
| Proceso principal + proxy hacia un servicio externo | Sí | El proxy es infraestructura local del proceso |
| Recolector de logs de todo el nodo | No | Eso es un DaemonSet (06-02) |
Tres preguntas de control antes de añadir un contenedor a un pod existente:
- ¿Necesitan escalar juntos siempre? Si la respuesta es no, son dos cargas de trabajo distintas.
- ¿El segundo sirve exclusivamente al primero? Si otros pods también lo necesitan, es un Service aparte.
- ¿Necesitan compartir
localhosto un sistema de ficheros? Si no, no hay motivo técnico para juntarlos.
Kubernetes ofrece dos categorías de contenedor auxiliar:
initContainers: se ejecutan antes, en orden, uno tras otro, y deben terminar con éxito.- Contenedores auxiliares que acompañan al principal durante toda la vida del pod: los sidecars.
- Init containers: preparación secuencial antes del arranque
Un init container es un contenedor que se ejecuta hasta completarse antes de que arranque ningún contenedor principal. Si hay varios, se ejecutan en el orden en que aparecen en el manifiesto, cada uno esperando a que el anterior termine con éxito.
graph LR A[Pod programado] --> I1[initContainer 1<br/>esperar-postgres] I1 -->|exit 0| I2[initContainer 2<br/>migrar-esquema] I2 -->|exit 0| C[containers<br/>api arranca] I1 -->|exit != 0| R1[reinicio del initContainer] R1 --> I1
Propiedades que los distinguen de los contenedores normales:
| Propiedad | Init container | Contenedor principal |
|---|---|---|
| Momento de ejecución | Antes de todos los principales | Después de todos los init |
| Ejecución | Secuencial, uno tras otro | Todos en paralelo |
| Debe terminar | Sí, con código 0 | No; terminar es una anomalía |
| Si falla | Se reintenta según restartPolicy del pod |
Se reinicia según restartPolicy |
| Sondas | No admite readinessProbe ni startupProbe |
Sí |
| Imagen | Suele ser distinta y con más herramientas | La de la aplicación |
Esa última fila es más útil de lo que parece: el init container puede usar una imagen con psql, curl o git que la imagen de producción no tiene, sin engordar la imagen final ni ampliar su superficie de ataque.
Sintaxis básica:
spec:
template:
spec:
initContainers:
- name: primero
image: busybox:1.36
command: ["sh", "-c", "echo preparando; sleep 2"]
- name: segundo
image: busybox:1.36
command: ["sh", "-c", "echo listo"]
containers:
- name: api
image: registry.rutasnorte.example/api-reservas:2.5.0Los init containers también consumen recursos y participan en el cálculo de lo que el pod pide al scheduler. La fórmula efectiva es:
petición del pod = max( mayor petición entre los initContainers ,
suma de peticiones de los containers )Es decir, un init container que pide 2 GiB obliga al scheduler a encontrar un nodo con 2 GiB libres, aunque la aplicación solo necesite 256 MiB. Conviene mantenerlos ligeros.
- Casos de uso de init containers en Rutas Norte
Caso 1: esperar a que postgres-reservas acepte conexiones
Cuando se levanta un entorno desde cero, api-reservas puede arrancar antes de que la base de datos esté lista, fallar al conectar, salir y entrar en CrashLoopBackOff. Acaba recuperándose, pero el arranque tarda minutos y los eventos se llenan de ruido que enmascara problemas reales.
initContainers:
- name: esperar-postgres
image: postgres:16.4
command:
- /bin/sh
- -c
- |
echo "Esperando a postgres-reservas..."
until pg_isready -h postgres-reservas -p 5432 -U "${PGUSER}" -t 3; do
echo " todavía no responde; reintento en 2 s"
sleep 2
done
echo "postgres-reservas acepta conexiones"
env:
- name: PGUSER
valueFrom:
secretKeyRef:
name: postgres-reservas-credenciales
key: usuario
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mipg_isready es una utilidad de la propia imagen de PostgreSQL que devuelve 0 si el servidor acepta conexiones. El bucle no tiene límite de intentos a propósito: el pod se quedará en Init:0/1 indefinidamente, lo que en kubectl get pods es una señal clarísima de "la base de datos no está disponible", mucho mejor que un CrashLoopBackOff cuya causa hay que ir a buscar en los logs.
Una nota de diseño honesta: esto no sustituye a que la aplicación gestione la reconexión. Si postgres-reservas se cae dos horas después, el init container ya no está para ayudar. Es una comodidad para el arranque, no una garantía de resiliencia.
Caso 2: ejecutar la migración de esquema
Aquí retomamos la pregunta que dejó abierta la lección 06-03. La migración puede ir en un init container en lugar de en un Job separado:
initContainers:
- name: esperar-postgres
# ... como arriba ...
- name: migrar-esquema
image: registry.rutasnorte.example/api-reservas-migraciones:2.5.0
command: ["/app/migrar", "--hasta=2.5.0"]
env:
- name: PGHOST
value: postgres-reservas
- name: PGUSER
valueFrom:
secretKeyRef:
name: postgres-reservas-credenciales
key: usuario
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: postgres-reservas-credenciales
key: password
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256MiVentaja evidente: la migración va acoplada al despliegue. Es imposible desplegar el código 2.5.0 sin haber migrado, porque el contenedor de la aplicación no arranca si el init container no termina bien.
Pero hay una trampa importante que hay que conocer:
Con 4 réplicas de
api-reservas, el init container de migración se ejecuta 4 veces, potencialmente en paralelo durante una actualización.
Consecuencias y mitigaciones:
- La migración debe ser idempotente y estar protegida por un lock.
IF NOT EXISTSno basta: dosALTER TABLEsimultáneos pueden bloquearse mutuamente. Una herramienta de migraciones seria (Flyway, Liquibase,golang-migrate) toma un lock de aplicación en la base de datos y las demás instancias esperan. - Si la migración tarda minutos, cada réplica paga esa espera, y el despliegue se alarga.
| Enfoque | Ventaja | Inconveniente |
|---|---|---|
| Job separado (06-03) | Se ejecuta una vez; control explícito | Hay que acordarse de lanzarlo antes; se puede olvidar |
| initContainer en el Deployment | Imposible desplegar sin migrar | Se ejecuta por réplica; exige lock e idempotencia |
Recomendación para Rutas Norte: Job separado para migraciones grandes o destructivas (índices sobre millones de filas, cambios de tipo), initContainer para migraciones pequeñas e idempotentes del día a día.
Caso 3: preparar ficheros en un emptyDir compartido
tienda-web sirve HTML estático desde nginx, y la plantilla del pie de página cambia según el entorno. En vez de construir tres imágenes, un init container genera el fichero en un volumen compartido:
apiVersion: apps/v1
kind: Deployment
metadata:
name: tienda-web
namespace: rutas-norte-pro
labels:
app: tienda-web
app.kubernetes.io/part-of: rutas-norte
entorno: pro
spec:
replicas: 3
selector:
matchLabels:
app: tienda-web
entorno: pro
template:
metadata:
labels:
app: tienda-web
app.kubernetes.io/part-of: rutas-norte
entorno: pro
spec:
automountServiceAccountToken: false
initContainers:
- name: preparar-contenido
image: busybox:1.36
command:
- sh
- -c
- |
set -eu
cp /plantillas/index.html /publico/index.html
sed -i "s|__ENTORNO__|${ENTORNO}|g" /publico/index.html
sed -i "s|__NODO__|${NODO}|g" /publico/index.html
echo "Contenido preparado para el entorno ${ENTORNO}"
env:
- name: ENTORNO
value: "pro"
- name: NODO
valueFrom:
fieldRef:
fieldPath: spec.nodeName
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: plantillas
mountPath: /plantillas
readOnly: true
- name: publico
mountPath: /publico
containers:
- name: nginx
image: nginx:1.27.2-alpine
ports:
- name: http
containerPort: 80
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 300m
memory: 128Mi
volumeMounts:
- name: publico
mountPath: /usr/share/nginx/html
readOnly: true
volumes:
- name: plantillas
configMap:
name: tienda-web-plantillas
- name: publico
emptyDir: {}El mecanismo es exactamente el emptyDir de 05-01: un volumen vacío creado con el pod, compartido por todos sus contenedores. El init container escribe, nginx lee en solo lectura. Cuando el pod muere, el emptyDir desaparece, y no importa: se regenera en el arranque siguiente.
- Diagnóstico de init containers fallidos
Los init containers tienen sus propios estados en la columna STATUS, y saber leerlos ahorra mucho tiempo.
STATUS |
Significado |
|---|---|
Init:0/2 |
Ejecutando el primer init container de dos; ninguno completado |
Init:1/2 |
El primero terminó bien; ejecutando el segundo |
Init:Error |
Un init container salió con código distinto de 0 y restartPolicy: Never |
Init:CrashLoopBackOff |
Un init container falla repetidamente con restartPolicy: Always |
PodInitializing |
Todos los init terminaron; arrancando los principales |
Running |
Todo en marcha |
Un caso real: api-reservas atascada porque la base de datos no responde.
Init:0/2 sostenido cuatro minutos: el primer init container (esperar-postgres) sigue en su bucle.
Esperando a postgres-reservas...
todavía no responde; reintento en 2 s
todavía no responde; reintento en 2 s
todavía no responde; reintento en 2 sLa clave está en -c <nombre>. Sin ese flag, kubectl logs intenta leer el contenedor principal, que aún no existe, y responde con un error confuso.
Otro caso: la migración falla.
Aplicando migración 2.5.0...
ERROR: column "canal_venta" of relation "reservas" already exists (SQLSTATE 42701)
migración fallidaDiagnóstico: la migración no es idempotente. Faltaba el IF NOT EXISTS.
Comandos útiles para inspeccionar init containers:
# Nombres de los init containers de un pod
kubectl get pod <pod> -n <ns> \
-o jsonpath='{range .spec.initContainers[*]}{.name}{"\n"}{end}'
# Estado detallado de cada uno
kubectl get pod <pod> -n <ns> -o jsonpath='{.status.initContainerStatuses}' | python3 -m json.tool
# Vista completa, con la sección "Init Containers" separada
kubectl describe pod <pod> -n <ns>En la salida de describe, los init containers aparecen en un bloque Init Containers: antes del bloque Containers:, cada uno con su estado, su código de salida y su razón de terminación.
- Sidecars nativos: init containers con
restartPolicy: Always
restartPolicy: AlwaysDurante años, un sidecar era simplemente "otro contenedor más en la lista containers". Funcionaba, pero tenía dos defectos graves que Kubernetes 1.29 resolvió y que están estables desde 1.33.
Los dos problemas del sidecar clásico
Problema 1: no hay orden de arranque. Todos los contenedores de containers arrancan en paralelo. Si el sidecar es un proxy por el que la aplicación tiene que salir a la red, y la aplicación arranca antes que el proxy, las primeras peticiones fallan.
Problema 2: los Jobs no terminaban nunca. Un Job cuyo pod tiene un sidecar era un problema sin solución limpia. El contenedor principal termina con éxito, pero el sidecar sigue vivo, así que el pod nunca alcanza Succeeded y el Job se queda colgado indefinidamente. La única salida eran apaños feos: ficheros centinela en un emptyDir, o que el contenedor principal matara al sidecar por la API.
La solución: restartPolicy: Always en un init container
initContainers:
- name: exportador-metricas
image: prometheuscommunity/postgres-exporter:v0.16.0
restartPolicy: Always # <- esto lo convierte en sidecar nativo
ports:
- name: metricas
containerPort: 9187Ese único campo cambia por completo la semántica del contenedor:
| Init container normal | Sidecar nativo (restartPolicy: Always) |
Contenedor de containers |
|
|---|---|---|---|
| Momento de arranque | En orden, antes que todo | En orden, antes de los principales | En paralelo con los demás |
| ¿Bloquea al siguiente? | Sí, hasta terminar | No: basta con que esté iniciado | No aplica |
| Duración | Termina | Vive todo el pod | Vive todo el pod |
| Si sale | Se pasa al siguiente | Se reinicia | Se reinicia |
| Al terminar el pod | Ya no está | Se para después de los principales | Se para en paralelo |
| ¿Bloquea la finalización de un Job? | No | No | Sí |
Las cuatro consecuencias prácticas, en orden de importancia:
- Arranca antes que los contenedores principales, garantizando que el proxy o el exportador estén listos cuando la aplicación empieza a trabajar.
- Sigue vivo durante toda la vida del pod y se reinicia si muere, cosa que un init container normal no hace.
- Se termina después de los contenedores principales, así que un sidecar de logs captura los últimos mensajes del cierre.
- No impide que un Job termine. Cuando los contenedores de
containersacaban, el kubelet para los sidecars y el pod alcanzaSucceeded. Esto desbloquea todo el trabajo por lotes con sidecars: un CronJob con proxy, con exportador de métricas o con adaptador de logs simplemente funciona.
Una precisión sobre el orden: el pod no espera a que el sidecar termine (nunca lo hará), sino a que esté iniciado —y a que pase su startupProbe si la tiene—. A diferencia de los init containers normales, los sidecars sí admiten sondas, tema de la lección 07-01.
Regla de decisión sencilla: si el contenedor auxiliar debe estar listo antes que la aplicación, o si el pod es de un Job, usa sidecar nativo. En los demás casos, un contenedor normal en containers sigue siendo perfectamente válido.
- Los tres patrones clásicos: sidecar, embajador y adaptador
Los tres nombres vienen del artículo fundacional de Brendan Burns y Dave Oppenheimer sobre patrones de diseño para sistemas distribuidos. Se distinguen por hacia dónde va el flujo y qué transforman.
| Patrón | Qué hace | Dirección del flujo | Ejemplo en Rutas Norte |
|---|---|---|---|
| Sidecar | Añade una capacidad que la aplicación no tiene, sin modificarla | Lateral: observa o complementa | Exportador de métricas de postgres-reservas |
| Embajador (ambassador) | Intermedia la salida hacia un servicio externo | Aplicación → exterior | Proxy hacia pagos.proveedorexterno.example |
| Adaptador (adapter) | Normaliza la salida de la aplicación a un formato estándar | Aplicación → exterior, transformando | Convertir el log propietario de worker-notificaciones a JSON |
Otra forma de memorizarlo:
- El sidecar añade algo.
- El embajador simplifica lo que la aplicación ve del mundo exterior.
- El adaptador simplifica lo que el mundo exterior ve de la aplicación.
Los tres se apoyan en los mismos dos mecanismos, que ya conoces de 02-01:
localhostcompartido: todos los contenedores del pod comparten el espacio de nombres de red, así que se hablan por127.0.0.1sin pasar por la red del clúster, sin DNS y sin latencia apreciable. Corolario: dos contenedores del mismo pod no pueden usar el mismo puerto.- Volúmenes compartidos: un
emptyDirmontado en ambos permite pasar ficheros. Es el canal del patrón adaptador.
graph TB
subgraph POD[Pod]
direction LR
APP[Contenedor principal]
SC[Sidecar<br/>añade capacidad]
EM[Embajador<br/>proxy de salida]
AD[Adaptador<br/>normaliza formato]
APP <-->|localhost| SC
APP -->|localhost:8080| EM
APP -->|emptyDir| AD
end
EM -->|TLS + reintentos| EXT[pagos.proveedorexterno.example]
SC -->|:9187/metrics| PROM[Prometheus 07-03]
AD -->|stdout JSON| LOGS[Recolector 06-02]
- Patrón sidecar: exportador de métricas junto a
postgres-reservas
postgres-reservasLa imagen postgres:16.4 no expone métricas en formato Prometheus. Modificarla sería un error: perderíamos las actualizaciones oficiales y tendríamos que mantener una imagen propia. La solución es un sidecar que se conecta a PostgreSQL por localhost, ejecuta consultas de estado y publica el resultado en /metrics.
Añadimos el sidecar al StatefulSet que construimos en 06-01:
# k8s/base/postgres-reservas-statefulset.yaml (fragmento)
spec:
template:
spec:
serviceAccountName: postgres-reservas
automountServiceAccountToken: false
initContainers:
- name: exportador-metricas
image: prometheuscommunity/postgres-exporter:v0.16.0
restartPolicy: Always # sidecar nativo
ports:
- name: metricas
containerPort: 9187
env:
# localhost: el sidecar comparte la red del contenedor de PostgreSQL
- name: DATA_SOURCE_URI
value: "localhost:5432/reservas?sslmode=disable"
- name: DATA_SOURCE_USER
valueFrom:
secretKeyRef:
name: postgres-reservas-credenciales
key: usuario
- name: DATA_SOURCE_PASS
valueFrom:
secretKeyRef:
name: postgres-reservas-credenciales
key: password
resources:
requests:
cpu: 20m
memory: 48Mi
limits:
cpu: 100m
memory: 96Mi
securityContext:
runAsNonRoot: true
runAsUser: 65534
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
containers:
- name: postgres
image: postgres:16.4
# ... resto igual que en 06-01 ...Puntos que explican el patrón:
DATA_SOURCE_URI: localhost:5432: no hay nombre de servicio ni DNS. El sidecar habla con PostgreSQL a través del bucle local del pod. Es la ventaja fundamental: latencia mínima, sin exponer el puerto 5432 a la red del clúster para esto, y sin necesidad de NetworkPolicy adicional, porque el tráfico ni siquiera sale del pod.- Sidecar nativo: arranca antes que PostgreSQL. En principio no es imprescindible aquí, pero garantiza que no perdemos las métricas de los primeros segundos de vida, que son justo las del arranque y la recuperación del WAL.
- Recursos modestos y separados: 20m de CPU y 48 MiB. Se suman a los de PostgreSQL en el cálculo del scheduler.
- Endurecimiento propio: el sidecar corre como
nobodyy con sistema de ficheros de solo lectura. No necesita nada más, y así un fallo en el exportador no compromete al contenedor de la base de datos.
Nota importante sobre QoS: en 03-05 establecimos que postgres-reservas es de clase Guaranteed. Para que el pod siga siéndolo, todos sus contenedores —sidecar incluido— deben tener requests iguales a limits. Con los valores de arriba (20m/100m, 48Mi/96Mi) el pod pasaría a Burstable. Si queremos conservar Guaranteed, hay que igualarlos:
Es una consecuencia poco intuitiva de añadir sidecars que conviene tener presente.
El Service que expone las métricas:
apiVersion: v1
kind: Service
metadata:
name: postgres-reservas-metricas
namespace: rutas-norte-pro
labels:
app: postgres-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: pro
spec:
selector:
app: postgres-reservas
entorno: pro
ports:
- name: metricas
port: 9187
targetPort: metricasComprobación:
kubectl exec -n rutas-norte-pro postgres-reservas-0 -c exportador-metricas -- \
wget -qO- localhost:9187/metrics | grep -E '^pg_up|^pg_stat_database_numbackends' | head -3pg_up 1 confirma que el exportador puede conectar; numbackends da las conexiones activas. Estas métricas son las que Prometheus recogerá en la lección 07-03; aquí solo hemos montado la fuente.
- Patrón embajador: la conexión con la pasarela de pagos
api-reservas cobra a través de pagos.proveedorexterno.example, un servicio externo con las complicaciones habituales: TLS mutuo con certificado de cliente, reintentos con retroceso, cortacircuitos cuando el proveedor va lento, límite de peticiones por segundo y un endpoint de pruebas distinto en cada entorno.
Meter toda esa lógica en api-reservas significa escribirla en Node.js, mantenerla, probarla y volver a hacerla si mañana aparece un segundo proveedor. El embajador la saca del código: un proxy local que escucha en localhost y se ocupa de todo.
# k8s/base/api-reservas-deployment.yaml (fragmento)
spec:
template:
spec:
serviceAccountName: api-reservas
automountServiceAccountToken: false
initContainers:
- name: embajador-pagos
image: envoyproxy/envoy:v1.31.3
restartPolicy: Always # sidecar nativo: debe estar listo antes que la API
args: ["-c", "/etc/envoy/envoy.yaml", "--log-level", "warn"]
ports:
- name: pagos-local
containerPort: 8081
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
securityContext:
runAsNonRoot: true
runAsUser: 65534
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: config-embajador
mountPath: /etc/envoy
readOnly: true
- name: certificados-pagos
mountPath: /etc/certificados
readOnly: true
containers:
- name: api
image: registry.rutasnorte.example/api-reservas:2.5.0
env:
# La aplicación habla HTTP plano contra localhost. Nada más.
- name: PASARELA_PAGOS_URL
value: "http://127.0.0.1:8081"
ports:
- name: http
containerPort: 3000
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
volumes:
- name: config-embajador
configMap:
name: embajador-pagos-config
- name: certificados-pagos
secret:
secretName: pagos-certificado-cliente
defaultMode: 0400Y la configuración del proxy, resumida a lo esencial:
# k8s/base/embajador-pagos-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: embajador-pagos-config
namespace: rutas-norte-pro
data:
envoy.yaml: |
static_resources:
listeners:
- name: pagos_local
address:
socket_address: { address: 127.0.0.1, port_value: 8081 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: pagos
route_config:
virtual_hosts:
- name: pagos
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: pasarela_externa
timeout: 8s
retry_policy:
retry_on: "5xx,connect-failure,reset"
num_retries: 3
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: pasarela_externa
connect_timeout: 3s
type: LOGICAL_DNS
circuit_breakers:
thresholds:
- max_connections: 50
max_pending_requests: 20
load_assignment:
cluster_name: pasarela_externa
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: pagos.proveedorexterno.example
port_value: 443
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: pagos.proveedorexterno.example
common_tls_context:
tls_certificates:
- certificate_chain: { filename: /etc/certificados/tls.crt }
private_key: { filename: /etc/certificados/tls.key }Lo que ha ganado Rutas Norte:
| Responsabilidad | Antes: en api-reservas |
Ahora: en el embajador |
|---|---|---|
| TLS mutuo con certificado de cliente | Código Node.js + gestión de ficheros | Configuración declarativa |
| Reintentos ante 5xx y cortes | Librería y lógica propia | retry_policy |
| Cortacircuito | Librería y lógica propia | circuit_breakers |
| Tiempos de espera | Constantes repartidas por el código | timeout en un sitio |
| Endpoint distinto por entorno | Variable de entorno y condicionales | Un ConfigMap por entorno |
| Rotación del certificado | Redespliegue de la aplicación | Cambio del Secret |
Y sobre todo: api-reservas hace un POST HTTP plano a http://127.0.0.1:8081/cobros y ya está. En las pruebas locales de desarrollo, ese endpoint puede ser un simulador; la aplicación no nota la diferencia.
La NetworkPolicy de 04-06 que autoriza la salida a la pasarela sigue aplicándose al pod, no al contenedor, así que no cambia: el pod entero necesita permiso de salida al puerto 443 externo.
Una aclaración necesaria: si esta idea se aplica a todo el tráfico de todos los pods, con un plano de control que distribuye la configuración, ya no se llama embajador sino malla de servicios (Istio, Linkerd). Ese es territorio de la lección 08-04; aquí resolvemos un caso concreto sin adoptar una plataforma entera.
- Patrón adaptador: normalizar el log de
worker-notificaciones
worker-notificacionesworker-notificaciones es un componente heredado que escribe en un fichero, con un formato propio y con trazas multilínea cuando hay una excepción:
2026-08-05 03:14:22 | ENVIO_OK | reserva=4471 | [email protected] | ms=312
2026-08-05 03:14:25 | ENVIO_ERR | reserva=4472 | [email protected] | causa=SMTP timeout
at smtp.enviar (smtp.js:88)
at cola.procesar (cola.js:41)El recolector de logs del DaemonSet de 06-02 lee la salida estándar de los contenedores, no ficheros arbitrarios, y aunque los leyera, ese formato no es consultable: no hay campos, y una excepción se parte en tres entradas sin relación.
El adaptador resuelve las dos cosas: lee el fichero desde un volumen compartido, une las líneas de continuación y emite JSON estructurado por su propia salida estándar, donde el recolector sí lo recoge.
# k8s/base/worker-notificaciones-deployment.yaml (fragmento)
spec:
template:
spec:
automountServiceAccountToken: false
initContainers:
- name: adaptador-logs
image: fluent/fluent-bit:3.1.9
restartPolicy: Always # sidecar nativo: se para DESPUÉS del worker
resources:
requests:
cpu: 30m
memory: 48Mi
limits:
cpu: 100m
memory: 96Mi
volumeMounts:
- name: logs-worker
mountPath: /logs
readOnly: true
- name: config-adaptador
mountPath: /fluent-bit/etc
readOnly: true
containers:
- name: worker
image: registry.rutasnorte.example/worker-notificaciones:1.9.3
env:
- name: LOG_FICHERO
value: /logs/notificaciones.log
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
volumeMounts:
- name: logs-worker
mountPath: /logs
volumes:
- name: logs-worker
emptyDir:
sizeLimit: 256Mi # sin límite, un log desbocado llena el disco del nodo
- name: config-adaptador
configMap:
name: adaptador-logs-config# k8s/base/adaptador-logs-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: adaptador-logs-config
namespace: rutas-norte-pro
data:
fluent-bit.conf: |
[SERVICE]
Flush 3
Log_Level error
Parsers_File parsers.conf
[INPUT]
Name tail
Path /logs/notificaciones.log
Tag notificaciones
Parser worker_rutasnorte
Multiline.parser worker_traza
Refresh_Interval 5
[FILTER]
Name record_modifier
Match notificaciones
Record componente worker-notificaciones
Record entorno pro
[OUTPUT]
Name stdout
Match notificaciones
Format json_lines
parsers.conf: |
[PARSER]
Name worker_rutasnorte
Format regex
Regex ^(?<time>[\d-]+ [\d:]+) \| (?<nivel>\w+) \| reserva=(?<reserva>\d+) \| destino=(?<destino>[^ ]+) \|(?<resto>.*)$
Time_Key time
Time_Format %Y-%m-%d %H:%M:%S
[MULTILINE_PARSER]
Name worker_traza
Type regex
Flush_Timeout 1000
Rule "start_state" "^\d{4}-\d{2}-\d{2} " "cont"
Rule "cont" "^ at " "cont"Resultado en la salida estándar del adaptador, que es lo que el recolector del nodo se lleva:
{"date":1754363665.0,"nivel":"ENVIO_ERR","reserva":"4472","destino":"[email protected]","resto":" causa=SMTP timeout\n at smtp.enviar (smtp.js:88)\n at cola.procesar (cola.js:41)","componente":"worker-notificaciones","entorno":"pro"}La excepción viaja entera en un solo registro, con sus campos separados y etiquetada con el componente y el entorno. En 07-05 esto permitirá consultas del tipo "todos los ENVIO_ERR de la reserva 4472".
Aquí el sidecar nativo aporta algo concreto e importante: como se termina después del contenedor principal, captura los últimos mensajes que worker-notificaciones escribe durante su cierre ordenado con SIGTERM (02-01). Con un contenedor normal en containers, ambos reciben SIGTERM a la vez y esas últimas líneas —a menudo las que explican por qué se paró— se pierden.
El sizeLimit: 256Mi del emptyDir no es opcional: un emptyDir sin límite escribe en el disco del nodo hasta llenarlo, y entonces el nodo entra en disk-pressure y desaloja pods ajenos.
- Orden de arranque y terminación, y el coste real
La secuencia completa
Con init containers y sidecars nativos, el ciclo de vida de un pod queda así:
sequenceDiagram
participant K as kubelet
participant I as initContainers normales
participant S as sidecars (restartPolicy Always)
participant C as containers principales
K->>I: arranca en orden; espera a que cada uno termine con éxito
I-->>K: exit 0
K->>S: arranca en orden; espera a que estén iniciados
S-->>K: iniciado (y startupProbe superada si la hay)
K->>C: arranca todos en paralelo
Note over C: vida útil del pod
K->>C: SIGTERM a los principales
C-->>K: terminados (o vencido el periodo de gracia)
K->>S: SIGTERM a los sidecars, en orden inverso
S-->>K: terminados
Puntos a retener:
- Los init containers normales se ejecutan secuencialmente y hasta terminar.
- Los sidecars nativos arrancan en el orden declarado, y basta con que estén iniciados.
- Los contenedores principales arrancan todos a la vez, sin garantía de orden entre ellos.
- En la terminación, primero los principales; después los sidecars, en orden inverso.
- El
terminationGracePeriodSecondses del pod, no de cada contenedor: es el presupuesto total para todo el cierre.
El coste real
Cada sidecar es un contenedor más por cada pod, y esa multiplicación es fácil de subestimar. Números de Rutas Norte en producción:
| Componente | Réplicas | Sidecar | CPU sidecar | Memoria sidecar | Total CPU | Total memoria |
|---|---|---|---|---|---|---|
api-reservas |
6 | Embajador de pagos | 50m | 64Mi | 300m | 384Mi |
worker-notificaciones |
3 | Adaptador de logs | 30m | 48Mi | 90m | 144Mi |
postgres-reservas |
1 | Exportador de métricas | 100m | 96Mi | 100m | 96Mi |
| Total | 0,49 CPU | 624Mi |
Medio núcleo y 600 MiB solo en contenedores auxiliares. En un pico de puente festivo, con api-reservas autoescalada a 20 réplicas (09-01), el embajador solo ya son 1 CPU y 1,25 GiB.
Y hay costes menos visibles:
- Cada sidecar es una imagen que hay que mantener, escanear y actualizar (08-05). Tres sidecars son tres cadenas de suministro más.
- Cada sidecar puede fallar y, con
restartPolicy: Always, entrar en bucle de reinicio arrastrando al pod entero. - Cada sidecar suma tiempo al arranque, y los nativos lo suman de forma secuencial antes de la aplicación.
- La depuración se complica: todo
kubectl logsykubectl execnecesita ya el flag-c.
Preguntas de control antes de añadir un sidecar:
- ¿Puede hacerlo un DaemonSet, uno por nodo en lugar de uno por pod? Para logs y métricas de nodo, casi siempre sí.
- ¿Puede hacerlo la propia aplicación con una librería? A veces cinco líneas de código sustituyen a un contenedor de 60 MiB.
- ¿Justifica el sidecar su coste multiplicado por el número máximo de réplicas? Calcula el peor caso, no el habitual.
Errores Comunes y Consejos
Olvidar -c <contenedor> en kubectl logs y kubectl exec. Con varios contenedores, kubectl exige saber cuál. El error a container name must be specified es inequívoco, pero cuando hay init containers el mensaje puede despistar porque el contenedor principal aún no existe.
Poner un init container en bucle infinito sin visibilidad. Un until ... done sin echo deja el pod en Init:0/1 sin ningún log que explique la espera. Imprime siempre algo en cada iteración.
Init containers pesados. El scheduler reserva el máximo entre los init containers, así que uno que pida 2 GiB obliga a encontrar un nodo con 2 GiB libres aunque la aplicación necesite 256 MiB. Mantenlos pequeños.
Migraciones en initContainer sin lock. Con N réplicas, la migración se ejecuta N veces, potencialmente en paralelo. Sin idempotencia y sin lock de aplicación, el resultado es una base de datos a medio migrar. Usa una herramienta de migraciones que tome lock, o un Job separado (06-03).
Dos contenedores del mismo pod escuchando en el mismo puerto. Comparten el espacio de red, así que el segundo falla con address already in use. Lleva un registro de qué puerto usa cada contenedor auxiliar (3000 la API, 8081 el embajador, 9187 el exportador...).
Sidecar clásico en un pod de Job. Es la trampa histórica: el Job no termina nunca. La solución en 1.29+ es un sidecar nativo con restartPolicy: Always como init container.
emptyDir sin sizeLimit para logs. Un log que crece sin control llena el disco del nodo y provoca disk-pressure, con desalojo de pods que no tenían nada que ver. Pon siempre sizeLimit.
Romper la clase QoS al añadir un sidecar. Un pod es Guaranteed solo si todos sus contenedores tienen requests == limits. Añadir un sidecar con valores distintos degrada el pod a Burstable y cambia su prioridad de desalojo (03-05).
Consejo: nombra los contenedores por su función, no por su tecnología. embajador-pagos es mucho más útil en una alerta a las tres de la mañana que envoy.
Consejo: kubectl describe pod es la mejor vista. Muestra Init Containers: y Containers: en bloques separados, con el estado, el código de salida y la razón de cada uno. Es más rápido que encadenar jsonpath.
Consejo: kubectl logs --all-containers=true vuelca todo el pod de golpe, útil para reconstruir la secuencia de un arranque problemático.
Ejercicios
Ejercicio 1: init container que espera a una dependencia
En rutas-norte-dev, crea un Deployment api-demo con una réplica de nginx:1.27.2-alpine y un init container que espere a que exista y responda un Service llamado dependencia-demo. Aplica el Deployment antes de crear la dependencia y observa el estado del pod. Después crea la dependencia (un Deployment y un Service con nginx) y comprueba que api-demo arranca.
Ejercicio 2: patrón adaptador con emptyDir compartido
En rutas-norte-dev, crea un Deployment worker-demo con:
- Un contenedor principal
workerque cada 5 segundos escriba una línea en formato propietario en/logs/salida.log(por ejemplo2026-08-05 10:00:00 | ENVIO_OK | reserva=4471). - Un sidecar nativo
adaptador(init container conrestartPolicy: Always) que siga ese fichero y emita cada línea por su salida estándar con el prefijo[adaptado]. - Un
emptyDirconsizeLimit: 64Micompartido.
Verifica que el sidecar arranca antes que el principal y que emite las líneas.
Ejercicio 3: sidecar nativo en un Job
Crea en rutas-norte-dev un Job informe-con-sidecar cuyo pod tenga un contenedor principal que tarde 15 segundos y termine, y un sidecar nativo que escriba algo cada 3 segundos indefinidamente. Comprueba que el Job sí llega a Complete. Después razona qué habría pasado si el sidecar estuviera declarado en containers en lugar de como init container con restartPolicy: Always.
Soluciones
Solución 1
# /tmp/api-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-demo
namespace: rutas-norte-dev
labels:
app: api-demo
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
replicas: 1
selector:
matchLabels:
app: api-demo
entorno: dev
template:
metadata:
labels:
app: api-demo
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
automountServiceAccountToken: false
initContainers:
- name: esperar-dependencia
image: busybox:1.36
command:
- sh
- -c
- |
echo "Esperando a dependencia-demo:80..."
until wget -q -T 2 -O /dev/null http://dependencia-demo:80 2>/dev/null; do
echo " aún no responde; reintento en 3 s"
sleep 3
done
echo "dependencia-demo disponible"
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 50m
memory: 32Mi
containers:
- name: nginx
image: nginx:1.27.2-alpine
ports:
- containerPort: 80
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128MiInit:0/1: el init container está corriendo y ninguno ha completado.
Esperando a dependencia-demo:80...
aún no responde; reintento en 3 s
aún no responde; reintento en 3 sAhora la dependencia:
kubectl create deployment dependencia-demo -n rutas-norte-dev --image=nginx:1.27.2-alpine
kubectl label deployment dependencia-demo -n rutas-norte-dev app=dependencia-demo entorno=dev --overwrite
kubectl expose deployment dependencia-demo -n rutas-norte-dev --port=80
kubectl wait --for=condition=ready pod -l app=api-demo -n rutas-norte-dev --timeout=120s
kubectl get pods -n rutas-norte-dev -l app=api-demoEl pod pasó de Init:0/1 a PodInitializing y luego a Running sin ningún reinicio. Ese RESTARTS: 0 es la mejora frente a dejar que la aplicación entre en CrashLoopBackOff mientras espera.
Solución 2
# /tmp/worker-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: worker-demo
namespace: rutas-norte-dev
labels:
app: worker-demo
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
replicas: 1
selector:
matchLabels:
app: worker-demo
entorno: dev
template:
metadata:
labels:
app: worker-demo
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
automountServiceAccountToken: false
initContainers:
- name: adaptador
image: busybox:1.36
restartPolicy: Always # sidecar nativo
command:
- sh
- -c
- |
echo "[adaptador] arrancado antes que el worker"
touch /logs/salida.log
tail -F /logs/salida.log | while read -r LINEA; do
echo "[adaptado] $LINEA"
done
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 50m
memory: 32Mi
volumeMounts:
- name: logs
mountPath: /logs
containers:
- name: worker
image: busybox:1.36
command:
- sh
- -c
- |
N=4471
while true; do
echo "$(date '+%Y-%m-%d %H:%M:%S') | ENVIO_OK | reserva=$N" >> /logs/salida.log
N=$(( N + 1 ))
sleep 5
done
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: logs
mountPath: /logs
volumes:
- name: logs
emptyDir:
sizeLimit: 64Mikubectl apply -f /tmp/worker-demo.yaml
kubectl wait --for=condition=ready pod -l app=worker-demo -n rutas-norte-dev --timeout=120s
POD=$(kubectl get pod -n rutas-norte-dev -l app=worker-demo -o jsonpath='{.items[0].metadata.name}')
kubectl logs -n rutas-norte-dev "$POD" -c adaptador --tail=4[adaptador] arrancado antes que el worker
[adaptado] 2026-08-05 19:14:02 | ENVIO_OK | reserva=4471
[adaptado] 2026-08-05 19:14:07 | ENVIO_OK | reserva=4472
[adaptado] 2026-08-05 19:14:12 | ENVIO_OK | reserva=4473La primera línea confirma el orden: el sidecar imprimió su mensaje de arranque antes de que el worker escribiera nada, porque los sidecars nativos arrancan antes que los contenedores de containers. El contenedor principal, en cambio, no imprime nada por su salida estándar:
Todo su registro va al fichero, y solo llega al recolector del nodo gracias al adaptador. Ese es exactamente el propósito del patrón.
Solución 3
# /tmp/informe-con-sidecar.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: informe-con-sidecar
namespace: rutas-norte-dev
labels:
app: informes-ocupacion
entorno: dev
spec:
backoffLimit: 1
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app: informes-ocupacion
entorno: dev
spec:
restartPolicy: Never
automountServiceAccountToken: false
initContainers:
- name: metricas-lote
image: busybox:1.36
restartPolicy: Always # sidecar nativo: NO impide que el Job termine
command:
- sh
- -c
- 'while true; do echo "[metricas] latido $(date +%H:%M:%S)"; sleep 3; done'
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 50m
memory: 32Mi
containers:
- name: generador
image: busybox:1.36
command:
- sh
- -c
- 'echo "generando informe de ocupación..."; sleep 15; echo "informe generado"'
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mikubectl apply -f /tmp/informe-con-sidecar.yaml
kubectl wait --for=condition=complete job/informe-con-sidecar -n rutas-norte-dev --timeout=120s
kubectl get job informe-con-sidecar -n rutas-norte-devEl Job llega a Complete en 19 segundos pese a que el sidecar seguía imprimiendo latidos indefinidamente.
POD=$(kubectl get pod -n rutas-norte-dev -l job-name=informe-con-sidecar -o jsonpath='{.items[0].metadata.name}')
kubectl logs -n rutas-norte-dev "$POD" -c metricas-lote --tail=3
kubectl get pod "$POD" -n rutas-norte-dev[metricas] latido 19:22:14
[metricas] latido 19:22:17
[metricas] latido 19:22:20
NAME READY STATUS RESTARTS AGE
informe-con-sidecar-4kx2p 0/2 Completed 0 45sEl pod está Completed con sus dos contenedores parados.
Qué habría pasado con el sidecar en containers: el contenedor generador habría terminado con éxito a los 15 segundos, pero metricas-lote habría seguido vivo. Un pod solo alcanza la fase Succeeded cuando todos sus contenedores han terminado, así que el pod se habría quedado indefinidamente en Running con 1/2 contenedores listos, y el Job en 0/1 completions para siempre. Solo activeDeadlineSeconds lo habría cortado, y con estado Failed.
Ese era exactamente el problema histórico que los sidecars nativos resolvieron.
# Limpieza
kubectl delete -f /tmp/informe-con-sidecar.yaml
kubectl delete -f /tmp/worker-demo.yaml
kubectl delete -f /tmp/api-demo.yaml
kubectl delete deployment,service dependencia-demo -n rutas-norte-devConclusión
Un pod con varios contenedores es una herramienta potente y fácil de usar mal. La regla que la gobierna es que los procesos compartan pod solo cuando estén tan acoplados que no tenga sentido escalarlos ni desplegarlos por separado, y cuando uno exista para servir al otro.
Los init containers se ejecutan en orden, hasta completarse, antes de que arranque ningún contenedor principal, y en Rutas Norte nos sirven para esperar a postgres-reservas, aplicar migraciones pequeñas e idempotentes, y preparar contenido en un emptyDir compartido. Sus estados —Init:0/2, Init:Error, Init:CrashLoopBackOff— son diagnósticos por sí mismos, y kubectl logs -c <nombre> es el comando que hay que interiorizar.
Los sidecars nativos de Kubernetes 1.29+ son init containers con restartPolicy: Always: arrancan antes que los contenedores principales, viven durante todo el pod, se reinician si mueren, se paran después de los principales y —lo que zanjó un problema de años— no impiden que un Job termine.
Los tres patrones clásicos se distinguen por la dirección del flujo: el sidecar añade una capacidad (el exportador de métricas de postgres-reservas que Prometheus consumirá en 07-03); el embajador intermedia la salida hacia el exterior (el proxy que se ocupa de TLS mutuo, reintentos y cortacircuito contra pagos.proveedorexterno.example); el adaptador normaliza lo que la aplicación produce (el conversor a JSON del log propietario de worker-notificaciones). Los tres se apoyan en localhost y en volúmenes compartidos, y los tres cuestan recursos multiplicados por el número de pods, algo que hay que calcular en el peor caso de escalado.
Hasta aquí hemos decidido qué se ejecuta y cómo se compone cada pod. La pregunta que no hemos tocado es dónde: hasta ahora el kube-scheduler ha colocado nuestros pods donde ha querido, y eso ha bastado. Pero postgres-reservas debería estar en un nodo con disco SSD, las réplicas de api-reservas no deberían compartir nodo —si ese nodo cae, se cae la API entera—, y las cargas de análisis no deberían competir con la venta de billetes. Todo eso se controla con afinidad, taints y tolerations, y es el tema de la siguiente lección: Planificación.
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
