Lesson  in  Kubernetes 101

Init containers y sidecars

Varios contenedores en un mismo Pod: un init container que bloquea el arranque hasta que su dependencia responde, y un sidecar nativo que acompaña al contenedor principal durante toda su vida.

En la primera lección quedó una frase pendiente: un Pod puede tener más de un contenedor. Toca desarrollarla. Los contenedores de un Pod comparten red (mismo localhost) y pueden compartir volúmenes, y eso habilita dos patrones que verás en cualquier clúster real.

  • Un init container se ejecuta antes que los contenedores principales, uno detrás de otro, y debe terminar con éxito para que el Pod continúe. Sirve para preparar el terreno: esperar a una dependencia, migrar una base de datos, descargar configuración.
  • Un sidecar acompaña al contenedor principal durante toda su vida: recoge sus logs, hace de proxy, refresca un certificado.

Y aquí hay una novedad importante que cambia cómo se escriben los sidecars: desde las versiones recientes de Kubernetes, un sidecar es un init container con restartPolicy: Always. Suena raro y tiene toda la lógica del mundo, como verás en un momento.

Paso 1: Un Pod que espera a su dependencia

Crea app.yaml. Es un Pod con dos init containers y un contenedor principal:

cat << 'EOF' > app.yaml
apiVersion: v1
kind: Pod
metadata:
  name: app
  labels:
    app: app
spec:
  initContainers:
  - name: espera-backend
    image: ghcr.io/iximiuz/labs/nginx:alpine
    command: ["sh", "-c"]
    args:
    - |
      until wget -q -T 2 -O /dev/null http://backend; do
        echo "el backend todavía no responde, esperando..."
        sleep 2
      done
      echo "backend disponible"
  - name: sidecar-logs
    image: ghcr.io/iximiuz/labs/nginx:alpine
    restartPolicy: Always
    command: ["sh", "-c"]
    args:
    - |
      while true; do
        echo "$(date) latido del sidecar" >> /var/log/app/app.log
        sleep 5
      done
    volumeMounts:
    - name: logs
      mountPath: /var/log/app
  containers:
  - name: app
    image: ghcr.io/iximiuz/labs/nginx:alpine
    volumeMounts:
    - name: logs
      mountPath: /var/log/app
  volumes:
  - name: logs
    emptyDir: {}
EOF

El YAML, explicado con preguntas y respuestas

¿Por qué el sidecar está en initContainers y no en containers?

Porque un sidecar necesita dos garantías que un contenedor normal no da: arrancar antes que la aplicación (de poco sirve un recolector de logs que llega tarde) y terminar después (de poco sirve si se deja por el camino los últimos logs). Los init containers ya arrancan en orden y antes que nadie; solo faltaba decirle a Kubernetes que este no debe terminar. Eso es exactamente lo que significa restartPolicy: Always dentro de un init container: "no esperes a que acabe, este se queda".

¿Y cuál es la diferencia real con meterlo en containers, como se hacía antes?

Que un contenedor normal no tiene orden de arranque db, y que en los Jobs impedía terminar (el Pod nunca completaba porque el sidecar seguía corriendo). El sidecar nativo resuelve ambos problemas: arranca en orden, se apaga cuando el contenedor principal termina, y su ciclo de vida no cuenta para dar por acabado un Job.

¿Qué pasa si un init container falla?

Se reintenta según el restartPolicy del Pod, y el Pod no avanza. Verás la fase Init:0/2 o Init:Error. Es un bloqueo deliberado: si la dependencia no está, no tiene sentido arrancar la aplicación.

¿Se ejecutan los init containers en paralelo?

No, en orden y de uno en uno, en el orden en que aparecen en la lista. Los contenedores de containers, en cambio, arrancan todos a la vez.

¿Cómo se comunican el sidecar y la aplicación?

Por las dos vías que comparte un Pod: el volumen emptyDir que ambos montan (así viajan los logs de este ejemplo) y localhost, porque comparten espacio de red. Un proxy sidecar clásico intercepta el tráfico precisamente por eso.

Aplícalo y observa el bloqueo, que es lo interesante:

kubectl apply -f app.yaml
kubectl get pod app
kubectl logs app -c espera-backend

El Pod se queda en Init:0/2 y el init container repite su cantinela: nadie responde en http://backend. Normal, todavía no existe.

Paso 2: Levanta el backend y desbloquea al Pod

Crea el Deployment y el Service que el init container está esperando. A estas alturas ya lo haces con los ojos cerrados:

kubectl create deployment backend --image=ghcr.io/iximiuz/labs/nginx:alpine --port=80
kubectl expose deployment backend --port=80 --target-port=80

Y vuelve a mirar al Pod app, sin tocarlo:

kubectl get pod app --watch

En cuanto el Service responde, el init container termina con éxito, arranca el sidecar y detrás de él el contenedor principal. Init:0/2, Init:1/2, Running. Nadie ha recreado nada: el Pod estaba esperando, y ya está.

Paso 3: Comprueba que el sidecar trabaja

El sidecar lleva escribiendo su latido desde antes de que la aplicación existiera. Léelo desde los dos lados del Pod:

kubectl logs app -c sidecar-logs
kubectl exec app -c app -- cat /var/log/app/app.log

El segundo comando es el que importa: el contenedor principal ve el fichero que escribe otro contenedor, porque ambos montan el mismo emptyDir. En el mundo real la dirección suele ser la contraria (la aplicación escribe y el sidecar envía a Loki, Elastic o Datadog), pero la mecánica es exactamente esta.

Comprueba también cómo aparece en la contabilidad del Pod:

kubectl get pod app
kubectl get pod app -o jsonpath='{.status.initContainerStatuses}' | jq .

La columna READY dice 2/2: el sidecar cuenta como contenedor vivo, no como paso de inicialización terminado.

Note

💡 ¿Y si el sidecar se cae? Al llevar restartPolicy: Always, el kubelet lo reinicia sin tocar el contenedor principal. Compruébalo si te apetece: kubectl exec app -c sidecar-logs -- pkill -f sleep y vigila la columna RESTARTS.

Resumen

  • Un init container prepara el terreno y bloquea el arranque hasta terminar con éxito. Se ejecutan en orden.
  • Un sidecar es un init container con restartPolicy: Always: arranca antes que la aplicación, termina después y no impide que un Job termine.
  • Los contenedores de un Pod se comunican por volúmenes compartidos y por localhost.
Previous lesson
DaemonSet
Next lesson
Jobs y CronJobs