Durante la recuperación de varios clústeres OKD tras un apagón, encontré imágenes que CRI-O daba por presentes pero que el nodo no podía utilizar. En uno de los nodos, volver a descargar una imagen por su digest permitió recuperar OVN y Multus. La intervención dejó una pregunta para la siguiente recuperación: ¿qué pasa si varios nodos necesitan volver a descargar las mismas imágenes a la vez?
El relato del incidente está en OpenShift tras un apagón: las imágenes estaban, pero no existían. Los comandos de diagnóstico y reparación están en la guía de recuperación del almacenamiento de CRI-O.
Monté Zot como proxy caché de imágenes OCI en un servidor RHEL fuera de los clústeres. La primera descarga de una imagen que falta en la caché sigue dependiendo del registro de origen. A partir de ahí, otros nodos y clústeres pueden reutilizar la copia interna. La caché ofrece otra ruta para obtener una imagen durante la recuperación. La reparación del almacenamiento local de CRI-O sigue siendo necesaria cuando presenta inconsistencias.
Los nombres de servidores, nodos, contextos y dominios internos son ficticios. Los nombres de los registros públicos se han conservado.
Un registro compartido fuera de los clústeres
Instalé Zot 2.1.23 en RHEL 9.7 x86_64 como servicio nativo de systemd. Los nodos acceden a registry.example.net:5000 por HTTPS. El nombre de servicio es un alias DNS del servidor RHEL y el certificado está firmado por una CA en la que confían los nodos.
Los manifiestos siguen usando las referencias originales. Por ejemplo, un pod pide docker.io/library/busybox:1.36 y CRI-O prueba registry.example.net:5000/docker.io/library/busybox:1.36. El prefijo docker.io identifica el origen al que Zot debe acudir si la imagen falta en la caché. Si la descarga desde el mirror falla, CRI-O puede probar directamente el registro de origen.
El mismo esquema se configuró para once registros: docker.io, quay.io, registry.k8s.io, ghcr.io, docker.elastic.co, gcr.io, eu.gcr.io, us.gcr.io, asia.gcr.io, europe-docker.pkg.dev, mcr.microsoft.com.
El almacenamiento NFS y el arranque de Zot
La caché reside en un montaje NFS 4.1 dedicado. En este ejemplo, la entrada de /etc/fstab queda así:
nfs-cache.example.net:/exports/oci-cache /srv/zot-cache nfs vers=4.1,hard,timeo=600,rsize=262144,wsize=262144,_netdev 0 0
sudo mkdir -p /srv/zot-cache
sudo mount /srv/zot-cache
findmnt --mountpoint /srv/zot-cache
El directorio del export debe permitir la escritura al usuario con el que se ejecuta Zot. También vinculé el servicio al montaje. Si el NFS no está montado, quiero que Zot se quede parado. Si arrancara sobre el directorio local que hay debajo, podría empezar a llenar el disco del servidor con una segunda caché.
Este fragmento de la unidad está adaptado a la ruta del ejemplo. El nombre de la unidad .mount cambia con el punto de montaje.
[Unit]
RequiresMountsFor=/srv/zot-cache
BindsTo=srv-zot-cache.mount
After=srv-zot-cache.mount
ConditionPathIsMountPoint=/srv/zot-cache
Configurar la descarga bajo demanda
El ejecutable está en /usr/bin/zot y lee /etc/zot/config.json. Este extracto muestra el almacenamiento, TLS y una de las once reglas de sincronización. Para Docker Hub, los manifiestos usan docker.io, mientras que Zot contacta con registry-1.docker.io.
{
"storage": {
"rootDirectory": "/srv/zot-cache"
},
"http": {
"address": "0.0.0.0",
"port": "5000",
"tls": {
"cert": "/etc/zot/tls/registry.crt",
"key": "/etc/zot/tls/registry.key"
}
},
"extensions": {
"sync": {
"enable": true,
"registries": [
{
"urls": [
"https://registry-1.docker.io"
],
"onDemand": true,
"tlsVerify": true,
"content": [
{
"prefix": "**",
"destination": "/docker.io"
}
]
}
]
}
}
}
Con la unidad zot.service y los certificados instalados, comprobé primero que el servicio arrancaba y que el endpoint OCI respondía por HTTPS.
sudo systemctl daemon-reload
sudo systemctl enable --now zot
sudo systemctl status zot
curl --noproxy '*' -fsS -o /dev/null -w '%{http_code}
' https://registry.example.net:5000/v2/
En mi instalación, /v2/ devolvió 200. Con eso comprobé la conexión HTTPS y la confianza en el certificado desde el cliente de prueba. Para comprobar la descarga desde un registro externo hacía falta pedir una imagen.
Redirigir las descargas de OpenShift
ImageDigestMirrorSet cubre referencias por digest (@sha256:…) e ImageTagMirrorSet cubre referencias por etiqueta (:1.36). Apliqué ambos con el mismo mapa de orígenes y destinos. Este manifiesto abreviado muestra la entrada de Docker Hub.
apiVersion: config.openshift.io/v1
kind: ImageDigestMirrorSet
metadata:
name: zot
spec:
imageDigestMirrors:
- source: docker.io
mirrors:
- registry.example.net:5000/docker.io
mirrorSourcePolicy: AllowContactingSource
---
apiVersion: config.openshift.io/v1
kind: ImageTagMirrorSet
metadata:
name: zot
spec:
imageTagMirrors:
- source: docker.io
mirrors:
- registry.example.net:5000/docker.io
mirrorSourcePolicy: AllowContactingSource
Mantuve los manifiestos en Git. Después de aplicarlos, comprobé que el Machine Config Operator había propagado el cambio a los nodos. Durante la migración también revisé los recursos existentes, porque parte del entorno ya tenía reglas para Nexus.
CTX=cluster-dev
oc --context="$CTX" apply --dry-run=server -f zot-mirrors.yaml
oc --context="$CTX" apply -f zot-mirrors.yaml
oc --context="$CTX" get imagedigestmirrorsets,imagetagmirrorsets
oc --context="$CTX" get mcp
oc --context="$CTX" get nodes
La prueba que confirmó el paso por la caché
Un crictl pull correcto, por sí solo, deja una duda. Con AllowContactingSource, CRI-O puede haber descargado la imagen directamente del origen. Por eso hice la prueba desde un nodo, usando el nombre público de la imagen, y miré a la vez el registro de peticiones de Zot.
oc --context=cluster-dev debug node/worker-01
Dentro de la sesión de diagnóstico:
chroot /host
curl --noproxy '*' -fsS -o /dev/null -w '%{http_code}
' https://registry.example.net:5000/v2/
grep -n 'registry.example.net' /etc/containers/registries.conf
crictl pull docker.io/library/busybox:1.36
En paralelo, en el servidor RHEL:
sudo journalctl -u zot -f
Los registros de Zot permitieron comprobar que la petición de CRI-O había llegado al mirror y observar la sincronización bajo demanda cuando faltaba contenido. En la primera fase validé la integración en desarrollo y preproducción, con los nodos listos y los MachineConfigPool convergidos. En desarrollo también retiré las reglas antiguas de Nexus. En producción, los manifiestos habían pasado la validación previa y quedaba aplicarlos. En los otros dos clústeres quedaba verificar la integración.
Qué cambia en el siguiente arranque
Si un nodo necesita volver a descargar una imagen que ya está en Zot, puede obtenerla por la red interna aunque su copia local haya quedado inutilizable. Si la imagen aún no está en la caché, Zot necesita llegar al origen. Y si falla Zot o su almacenamiento NFS, el acceso directo al origen sigue siendo posible siempre que el nodo tenga conectividad y el registro responda.
La comprobación que me llevo del incidente es concreta: para saber si una imagen crítica puede recuperarse a través de la caché, hay que pedirla desde CRI-O con su referencia original y comprobar que Zot atendió esa petición. Ver el registro levantado o ver un pull correcto solo verifica una parte del recorrido.
Documentación: sincronización y mirroring en Zot 2.1.23 y configuración de mirrors en OpenShift.