
🌐 También en: English · Deutsch · Français
Hora de confesar: el Nextcloud de producción del diario de batalla de la migración lleva una semana funcionando en K3s, el repositorio tiene un playbook nextcloud-update.yml desde hace meses, y yo nunca lo había ejecutado de verdad. Las apps se actualizaban a la antigua, haciendo clic en la interfaz de administración, o directamente no se actualizaban. Hoy eso cambió. La ejecución en sí tardó 2 minutos y 45 segundos. Lo interesante fue todo lo que había alrededor.
El mismo flujo de siempre: yo decido, mi coadministrador de IA (Claude) revisa, construye y verifica. Y lo primerísimo que encontró no fue un bug en la lógica de actualización, sino una pistola cargada apuntando a mi propio pie, en la línea tres.
🧭 ¿Para qué un playbook de actualización aparte?
Primero, una pregunta que realmente tuve que hacer: si ejecuto el playbook principal, ¿se actualizan mis apps? Respuesta: no, y es a propósito. Hay dos motivos independientes, y el playbook principal no cubre deliberadamente ninguno de ellos.
Motivo 1: mismo tag, imagen más nueva
Un tag de imagen como nextcloud:34.0.4-fpm no es más que un nombre que apunta a una imagen. Las imágenes oficiales se reconstruyen con el mismo nombre cada vez que se parchea algo por debajo: un parche de seguridad de Debian, una versión menor de PHP, OpenSSL. La versión de Nextcloud sigue siendo 34.0.4; el contenido, no.
Mis pods funcionan con imagePullPolicy: IfNotPresent: si ya hay una imagen con ese nombre en el almacén local de containerd, K3s nunca vuelve a preguntar al registro. Así que, mientras el tag del inventario no cambie, el playbook principal aplica manifiestos idénticos, K3s no ve nada que hacer y el build antiguo sigue funcionando indefinidamente. El playbook de actualización fuerza la comparación con crictl pull y reinicia después. Lo mismo para Collabora.
Motivo 2: las apps no están en la imagen
Este es el motivo de más peso. El directorio de código de Nextcloud, /var/www/html, es un volumen HostPath en el servidor (/srv/nextcloud-www), y las apps de la tienda de apps se instalan justo ahí, junto al núcleo. Una imagen nueva trae un núcleo nuevo y las apps incluidas. Nunca toca las apps de terceros como audioplayer, drawio o Deck. Esas solo se mueven con occ app:update (o con un clic en la interfaz de administración). El playbook principal solo impone la distribución de apps: estas activadas, estas desactivadas, instalar lo que falte. Hay un interruptor opcional (nextcloud_apps_update), por defecto false.
Tres capas, tres palancas
| ¿Qué cambia? | Ejemplo | Palanca |
|---|---|---|
| Versión de Nextcloud | 34.0.4 → 34.0.5, o → 35 | Cambiar el tag en el inventario, ejecutar nextcloud-k3s.yml |
| Imagen reconstruida, mismo tag | Parche de seguridad en PHP o Debian | nextcloud-update.yml (pull + reinicio) |
| Apps de la tienda | audioplayer 3 → 4 | nextcloud-update.yml (occ app:update --all) |
¿Por qué no meterlo todo en una sola ejecución? Porque el playbook principal tiene que ser idempotente y predecible: converge hacia un estado declarado, y ejecutarlo dos veces no cambia nada la segunda vez. Las actualizaciones son lo contrario: traen lo que haya de nuevo en internet y sustituyen código en ejecución. Eso merece su propio momento, su propia comprobación de las copias de seguridad y su propio vistazo a los logs después. Si no, cada pequeña ejecución de configuración actualizaría apps por lo bajini, y cuando algo se rompiera después, nunca sabrías si fue tu cambio de configuración o la versión publicada por otro.
🔫 El tiro en el pie: hosts: nextcloud
El play apunta al grupo de inventario nextcloud. Ese grupo contiene ahora mismo la instancia de producción, un host de pruebas y una segunda instancia que está en plena migración: su base de datos se sobrescribe en cada sincronización hasta el día del cambio definitivo y está fijada a la versión exacta del servidor antiguo. Olvida --limit una sola vez y la actualización arrasa con las tres, reiniciando pods en una máquina que debería estar completamente quieta.
El arreglo son cuatro líneas y hace que el playbook se niegue a arrancar sin un límite:
- name: Update | Refuse to run without --limit
ansible.builtin.assert:
that: ansible_limit is defined
fail_msg: "Run with --limit <host>, e.g. --limit cvjm - this playbook restarts Nextcloud."
run_once: trueansible_limit es una variable mágica que solo existe cuando se ha pasado --limit. Barato, explícito, y convierte un «ups» en un mensaje de error. Primera tarea de la ejecución de hoy: “All assertions passed”. 🎉
✅ Antes del despegue: cuatro comprobaciones, dos minutos
- ¿Copia de seguridad reciente? El pull diario desde la máquina de copias había terminado a las 12:30 con volcado de la base de datos y datos. Una actualización sin un punto de restauración reciente es una apuesta, no mantenimiento.
- ¿Qué va a cambiar de verdad?
occ app:update --all --showonlyes el ensayo en seco. Resultado: exactamente una app, audioplayer, que salta a la 4.0.0. - ¿Punto de partida sano?
occ status: sin modo de mantenimiento, sin actualización de base de datos pendiente, todos los pods funcionando, 190 GB libres. Cosas que quieres saber antes, para poder distinguir después si has roto algo o si ya estaba roto. - ¿Quién está conectado? Cinco usuarios activos en los últimos 15 minutos. Dos reinicios de Nextcloud y uno de Collabora significan que los documentos de Office abiertos se desconectan. Anotado, aceptado: es un martes tranquilo a la hora de comer.
⚙️ Anatomía de la ejecución
| Paso | Tiempo | Por qué está ahí |
|---|---|---|
crictl pull Nextcloud + Collabora | 51 s | Los pods usan imagePullPolicy: IfNotPresent, así que K3s nunca vuelve a descargar un tag por su cuenta. Aquí el tag está fijado a una versión de parche exacta, así que esto sobre todo confirmó que la imagen fijada sigue siendo la imagen fijada. |
| Reiniciar Nextcloud | 42 s | Recoge una imagen nueva si la hay. El deployment usa la estrategia Recreate: solo un pod puede tocar los volúmenes HostPath, así que hay un breve hueco en lugar de un relevo escalonado. |
| Reiniciar Collabora | 20 s | La misma idea para el servidor de Office. |
occ upgrade | 2 s | El entrypoint del contenedor ya lo ejecuta al arrancar; esto es la red de seguridad. |
occ app:update --all | 2 s | audioplayer → 4.0.0. La tienda de apps solo ofrece versiones compatibles con la versión mayor en ejecución, así que esto no puede colar nada que rompa Nextcloud 34. |
| Reiniciar Nextcloud otra vez | 43 s | El interesante, ver más abajo. |
🧠 ¿Por qué reiniciar dos veces? OPcache y un ajuste que no perdona
La configuración de PHP funciona con opcache.validate_timestamps=0. Es un ajuste de rendimiento: PHP compila cada archivo una vez y no vuelve a comprobar el archivo en disco durante toda la vida del proceso. Genial para la velocidad, pero con una consecuencia fea para las actualizaciones.
El primer reinicio ocurre antes de que occ escriba nada; está para recoger imágenes. Después, app:update sobrescribe los archivos PHP de la app en el volumen persistente, pero el PHP-FPM en ejecución sigue sirviendo el bytecode antiguo desde su caché. La actualización parece hecha, app:list muestra el nuevo número de versión, y el código antiguo sigue funcionando hasta el siguiente reinicio, que llegará por otro motivo cualquiera. Para un parche de seguridad, es el peor tipo de «hecho». De ahí el segundo reinicio, y solo cuando algo ha cambiado de verdad.
Detalle curioso: esto solo afecta a las apps actualizadas. Una app recién instalada nunca estuvo en la caché, así que se compila desde cero. Son precisamente las actualizaciones las que, en silencio, no surten efecto.
🐛 Bug n.º 2: una tarea que siempre decía «changed»
La tarea occ upgrade informaba de changed aunque no había nada que actualizar. Su changed_when buscaba la ausencia de la frase “already at the latest version”, y Nextcloud 34 ya no imprime esa frase. Cuando no hay nada que hacer, ahora solo muestra una nota que remite a la documentación de actualización. La ausencia de una cadena que ya no existe siempre es verdadera, así que la tarea siempre anunciaba un cambio.
# before: true forever on NC 34
changed_when: "'already at the latest version' not in occ_upgrade.stdout"
# after: only a real upgrade prints this
changed_when: "'Update successful' in occ_upgrade.stdout"Inofensivo hoy, porque la actualización de la app ya provocaba el segundo reinicio de todos modos. Pero un playbook que miente sobre sus cambios te acostumbra a ignorar su salida, y un día esa salida importará. Lección: comprobar la señal positiva, no la ausencia de una antigua.
🔍 Después del aterrizaje: confía, pero verifica
- audioplayer 4.0.0 activado; la lista de apps desactivadas, sin cambios, así que nada se activó ni desactivó a mis espaldas.
- nextcloud.log desde la ejecución: cero avisos, cero errores. Ningún error de PHP en el contenedor.
- Collabora: el discovery responde, el lado de Nextcloud de la conexión está intacto.
- Autoprueba de notify_push: las tres comprobaciones en verde. El servidor push se volvió a conectar solo tras los reinicios.
- 5xx en el ingress: cinco peticiones, todas de móviles y clientes de sincronización durante los segundos en que el pod se reiniciaba. Esperable con Recreate; los clientes reintentan automáticamente.
🎯 Conclusión
La actualización en sí fue lo menos emocionante: una app, menos de tres minutos, logs limpios. El valor estaba en los bordes. Una barandilla que hace imposible el comportamiento peligroso por defecto, un reinicio que existe por culpa de un único ajuste de caché y una comprobación de estado que llevaba mintiendo en silencio desde la última versión mayor. La infraestructura como código no hace que las actualizaciones sean mágicas. Las hace aburridas, y aburrido es exactamente lo que quieres de aquello que toca producción. 🧰




