Seleccione su idioma

Un corte eléctrico dejó varios clústeres OpenShift/OKD en un estado desconcertante: nodos aparentemente disponibles, pods bloqueados y componentes críticos que no podían crear contenedores. La causa no estaba en el registro remoto de imágenes, sino en referencias y capas inconsistentes del almacenamiento local de CRI-O. Esta guía recoge los comandos utilizados, los resultados observados y un procedimiento de recuperación controlado.

Nota: caso real anonimizado. Se han omitido nombres de nodos, redes, dominios, identificadores de imágenes y detalles específicos de la infraestructura. Los valores entre <...> deben sustituirse antes de ejecutar comandos. Se distinguen las acciones realmente observadas de las recomendaciones operativas.

La historia y el contexto de la incidencia están en OpenShift tras un apagón: las imágenes estaban, pero no existían.

1. Síntomas: un clúster que parecía arrancar

Tras el apagado inesperado aparecieron errores CreateContainerError, ImagePullBackOff, image not known y pods atascados en ContainerCreating. Multus y OVN-Kubernetes no llegaban a inicializarse; ciertos pods estáticos del plano de control tampoco. Varios operadores acabaron degradados y, más adelante, surgieron errores de TLS y de límite de descargas.

La primera inspección, desde un cliente con acceso al API, permitió delimitar el impacto:

oc get nodes
oc get co
oc get pods -A -o wide

Pero el estado Ready de un nodo o de un sandbox no demostraba que todos sus contenedores funcionasen. La investigación tuvo que bajar al runtime:

sudo systemctl is-active crio kubelet
sudo crictl pods -a
sudo crictl ps -a
sudo crictl images --digests

Para relacionar un sandbox con sus procesos, utilizamos:

sudo crictl ps -a --pod <SANDBOX_ID>

Encontramos sandboxes presentes cuyos contenedores no se habían creado. Ese detalle explica por qué comprobar únicamente los objetos Kubernetes era insuficiente.

2. La pista clave: CRI reconocía imágenes que el almacén no resolvía

Los registros contenían image not known y errores de rutas de capas desaparecidas, del tipo:

overlay/.../diff: no such file or directory

En uno de los nodos, crictl seguía localizando la referencia de una imagen:

sudo crictl images
sudo crictl inspecti <IMAGE_ID>

La comprobación correspondiente con Podman devolvía:

sudo podman image inspect <IMAGE_ID>
image not known

Interpretación: había referencias de imagen visibles para la interfaz CRI que no se correspondían con un objeto utilizable del almacén local. Una discrepancia aislada entre estas herramientas no demuestra corrupción: hay que confirmar que consultan el mismo almacén y correlacionarla con los errores de capas y creación de contenedores. En esta incidencia, las evidencias coincidían.

3. Diagnóstico de Multus y OVN-Kubernetes

En un worker, kubelet registraba:

multus-shim: CmdAdd (shim): timed out waiting for the condition

Había sandboxes de red, pero faltaban los contenedores correspondientes. Comprobamos:

sudo crictl ps -a --name multus
sudo crictl ps -a --name ovnkube
sudo ls -la /run/multus/cni/net.d/
sudo ls -la /run/multus/socket/

No estaban presentes los archivos esperados en tiempo de ejecución:

/run/multus/cni/net.d/10-ovn-kubernetes.conf
/run/multus/socket/multus.sock

La ausencia de esos artefactos era un síntoma: los componentes que debían generarlos no podían arrancar correctamente porque fallaba la resolución de sus imágenes. Reparar solamente Multus no habría corregido el problema de fondo.

4. Confirmación con la comprobación completa de CRI-O

CRI-O incluye una herramienta específica para verificar su almacenamiento:

sudo crio check
printf 'CRIO_CHECK_RC=%s\n' "$?"

En un nodo afectado, con kubelet detenido durante la comprobación, el resultado real fue:

Objeto Errores
Capas 71
Imágenes 70
Contenedores 225
CRIO_CHECK_RC=1

Atención a --quick: también se probó crio check --quick; su resultado satisfactorio no descartó los errores que sí mostró la comprobación completa. El modo rápido omite verificaciones y no debe utilizarse como prueba definitiva de integridad tras un apagado inesperado.

5. Reparación ordinaria y por qué falló

El primer intento se hizo con:

sudo crio check --repair

No consiguió resolver todas las referencias dañadas. Aparecieron errores como:

layer has children
image is in use by a container
CRIO_REPAIR_RC=1

Algunas capas e imágenes no podían retirarse porque persistían dependencias de contenedores afectados.

6. Reparación controlada con --force

La intervención que funcionó en ese nodo fue:

sudo crio check --repair --force

--repair retira imágenes y capas dañadas; --force permite además retirar contenedores dañados que impiden completar esa reparación. Puede eliminar objetos locales: no es un comando para ejecutar indiscriminadamente.

La documentación de CRI-O aconseja detener CRI-O y, cuando sea posible, sus contenedores durante estas operaciones para evitar accesos concurrentes. En un clúster hay que trabajar nodo por nodo, comprobar la redundancia y preservar el quorum de etcd, especialmente en nodos del plano de control.

Como procedimiento reutilizable —no como transcripción literal de cada nodo intervenido— la secuencia de mantenimiento sería:

# Desde un cliente con acceso al API, si está operativo:
oc adm cordon <NODE>

# En el nodo aislado, tras planificar el impacto:
sudo systemctl stop kubelet
sudo systemctl stop crio

# Solo cuando no haya accesos concurrentes al almacén:
sudo crio check --repair --force
sudo crio check

cordon evita programar nuevas cargas, pero no detiene por sí mismo los contenedores existentes. Detener CRI-O en producción exige una intervención controlada, no limitarse a copiar el bloque.

En la incidencia, la reparación forzada y la verificación posterior terminaron con:

CRIO_REPAIR_FORCE_RC=0
CRIO_CHECK_AFTER_RC=0

No utilizamos --wipe ni borramos indiscriminadamente /var/lib/containers/storage. El objetivo era eliminar las referencias corruptas y conservar las imágenes que todavía fueran válidas.

7. Restaurar imágenes por digest

Después de recuperar el almacén, algunas imágenes críticas se obtuvieron explícitamente mediante el digest exacto que requerían los componentes. Una de las operaciones reales siguió este patrón (digest anonimizado):

sudo systemctl start crio
sudo crictl pull \
  'quay.io/okd/scos-content@sha256:<DIGEST_COMPLETO>'

En un worker, la operación tardó alrededor de un minuto y devolvió Image is up to date. Después, CRI-O resolvió una referencia utilizable y comenzaron a ejecutarse contenedores de OVN-Kubernetes.

Hay que sustituir el marcador por el digest real requerido por el pod. No basta con descargar una versión reciente mediante un tag. Para validar la referencia:

sudo crictl inspecti \
  'quay.io/okd/scos-content@sha256:<DIGEST_COMPLETO>'
sudo crictl ps -a --name ovnkube
sudo crictl ps -a --name multus

Una descarga explícita puede reconstruir referencias inconsistentes, pero no reemplaza una comprobación completa del almacén cuando hay corrupción confirmada. Después de la recuperación de imágenes, se puede volver a arrancar kubelet:

sudo systemctl start kubelet

8. Validación funcional: no basta con que el pod aparezca en Running

En el nodo de red recuperado reaparecieron los contenedores de OVN y los artefactos de Multus que antes faltaban. Repetimos:

sudo crictl ps -a --name ovnkube
sudo crictl ps -a --name multus
sudo ls -l /run/multus/cni/net.d/10-ovn-kubernetes.conf
sudo ls -l /run/multus/socket/multus.sock
sudo journalctl -u crio -b --no-pager
sudo journalctl -u kubelet -b --no-pager

La recuperación del plano de control requirió también reconstruir contenedores que no arrancaban por image not known. Algunos errores de certificados y autenticación eran efectos secundarios de componentes estáticos todavía incompletos, no necesariamente una segunda causa raíz.

La comprobación general del clúster volvió a ser:

oc get nodes
oc get co
oc get pods -A -o wide

Finalmente se observó el operador kube-apiserver con AVAILABLE=True, PROGRESSING=False y DEGRADED=False. En los nodos que habían sido aislados, oc adm uncordon <NODE> solo debía ejecutarse después de validar los componentes críticos.

9. Errores secundarios: pull QPS exceeded

Al desbloquear la red y las imágenes, muchos pods pendientes reintentaron arrancar simultáneamente. Aparecieron ErrImagePull: pull QPS exceeded y algunos fallos transitorios de descarga. No implicaban que la registry remota fuera la causa del apagón: surgieron durante la recuperación masiva. Escalonar nodos, recuperar primero imágenes críticas y evitar reinicios masivos redujo el riesgo de amplificar la incidencia.

10. Checklist para una recuperación similar

  1. Delimitar: oc get nodes, oc get co y pods fallidos.
  2. Confirmar: inspeccionar CRI-O/kubelet; comparar crictl inspecti con podman image inspect cuando compartan almacén.
  3. Verificar integridad: utilizar crio check completo, sin confiar únicamente en --quick.
  4. Aislar: intervenir un nodo cada vez, con atención al quorum, las cargas existentes y la actividad concurrente.
  5. Reparar: probar --repair y, si persisten contenedores dañados que lo impiden, evaluar --repair --force.
  6. Volver a comprobar: exigir que el chequeo completo final termine correctamente antes de reactivar servicios.
  7. Restaurar: arrancar CRI-O, recuperar imágenes por digest y arrancar kubelet.
  8. Validar: comprobar CNI, contenedores reales, API, operadores y ausencia de errores repetidos.

Conclusión

La lección principal fue distinguir un fallo al descargar imágenes de un almacén local de imágenes inconsistente. Los tres comandos que más ayudaron a identificarlo fueron:

sudo crictl inspecti <IMAGE_ID>
sudo podman image inspect <IMAGE_ID>
sudo crio check

Y, una vez confirmada la corrupción, la reparación selectiva con crio check --repair --force —bajo condiciones controladas— permitió restaurar el almacenamiento sin borrarlo por completo. No hay que memorizar únicamente ese comando: hay que saber cuándo está justificado ejecutarlo y cómo verificar el resultado.

Referencias