
🌐 También en: English · Deutsch · Français
Parte 4 de la visita a Ansible. La parte 1 explicó el marco, la parte 2 el servidor y la plataforma, la parte 3 el blog. Esta parte es la segunda aplicación: Nextcloud. Describe la versión v3.3.0. Si has leído la parte 3, mucho te sonará – señalo las diferencias y explico lo nuevo, en lugar de repetir los patrones comunes.
Nextcloud es una nube de archivos: archivos, calendarios, contactos, carpetas compartidas y – con Collabora – documentos editados en grupo en el navegador. Dos de los tres servidores de aquí lo ejecutan para grupos de tamaños muy distintos, con el mismo código y otros valores en el inventario. El trabajo se reparte entre tres roles más el playbook de actualización:
| Rol / playbook | Qué hace |
|---|---|
next_mariadb, next_redis | Base de datos y caché, directamente en el host |
next_k3s_deploy | Nextcloud, Collabora, el servicio push y todo lo que los rodea como objetos de Kubernetes |
next_config | Los ajustes propios de Nextcloud, aplicados con occ – leer, comparar, escribir solo lo que difiere |
nextcloud_verify | Pruebas de humo al final de la ejecución |
nextcloud-update.yml | La actualización rutinaria, con comprobaciones de salud antes y después y un camino definido para cuando algo falla |
🏗️ Qué se ejecuta, y por qué
El camino de una petición es el mismo que en el blog: controlador Ingress (TLS) → sidecar de nginx → PHP-FPM, en un pod. Lo que cambia es todo lo que hay alrededor.
| Componente | Por qué está ahí |
|---|---|
| Nextcloud (PHP-FPM + nginx) | La aplicación en sí, de la imagen oficial nextcloud:fpm, con nginx como sidecar para los archivos estáticos – el mismo razonamiento que con WordPress. Actualizar Nextcloud significa cambiar la imagen; nada de PHP en el host. |
| Dos directorios del host | Los archivos de la aplicación (/srv/nextcloud-www) y los datos de los usuarios (/data) están separados a propósito. Una imagen nueva reemplaza los archivos de la aplicación; los archivos de los usuarios se quedan donde están y pueden respaldarse por separado, y una instalación nueva solo necesita un directorio de aplicación vacío. |
| MariaDB en el host | A diferencia del blog, la base de datos se ejecuta como un servicio normal en el host. Sus archivos de datos están directamente en el sistema de archivos del host, sin una capa de volumen de contenedor en medio – para una base de datos de este tamaño e importancia, es una cosa menos entre los datos y el disco. Los pods llegan a ella a través de un Service de Kubernetes que apunta al host (se explica más abajo). |
| Redis en el host | Nextcloud usa Redis para dos cosas: una caché compartida por todos los procesos PHP, y el bloqueo de archivos – para que dos clientes que guardan el mismo archivo en el mismo instante no lo corrompan. El bloqueo es imprescindible para una nube de archivos con clientes de sincronización. Una caché por proceso (APCu) se encarga de las pequeñas consultas locales; Redis de todo lo que debe ser coherente entre procesos. |
| Collabora Online (CODE) | Una suite ofimática que se ejecuta en el servidor y se muestra en el navegador. Nextcloud y Collabora hablan mediante WOPI, un protocolo en el que el editor pide al servidor de archivos «dame este archivo» y «guarda esta versión», firmado con una clave para que el servidor de archivos sepa que las peticiones son auténticas. Collabora se ejecuta en su propio pod, tras su propio nombre de host. |
| notify_push | Sin él, cada cliente de escritorio y móvil pregunta al servidor cada treinta segundos si algo ha cambiado. notify_push mantiene abierto un WebSocket y avisa a los clientes cuando cambia un archivo – menos carga, sincronización más rápida. Es un pequeño programa que viene dentro de una app de Nextcloud y se ejecuta como pod propio. |
| Un CronJob para las tareas de fondo | Nextcloud tiene una cola de trabajo en segundo plano: vistas previas, limpiezas, notificaciones, caducidad de versiones. Un CronJob ejecuta cron.php cada cinco minutos – el modo que el propio Nextcloud recomienda frente a «al cargar la página». |
| Vistas previas | Miniaturas para imágenes, vídeos y documentos, generadas en segundo plano por la app Preview Generator en lugar de con el primer clic. |
El contexto de esta instalación está repartido en artículos más antiguos: cómo funcionaban las instancias antes de K3s y la migración a K3s, paso a paso.
🗄️ next_mariadb: una base de datos en el host
El rol instala MariaDB desde el repositorio que configuró next_packages (parte 2), lo asegura y crea la base de datos de Nextcloud. Su cabecera indica el orden, y el orden explica el rol.
Sandbox antes del primer arranque
MariaDB recibe un drop-in de systemd con ajustes de sandbox (del files/ del rol): en qué directorios puede escribir, qué funciones del kernel no puede tocar. El comentario de cabecera hace una observación que merece repetirse: el mismo tipo de restricción no se aplicó a propósito a SSH en su forma más estricta, porque sshd inicia sesiones de usuario y todo lo que hiciera un usuario o Ansible la heredaría. MariaDB no inicia nada, así que una restricción solo afecta a la base de datos. Desde el 16/10/2026 su /tmp es un tmpfs privado de 4 GB, así que las tablas temporales y los archivos de ordenación se quedan en RAM. No debe combinarse con PrivateTmp=yes: probado con systemd-run, ese ajuste gana en silencio y deja /tmp en el disco.
El orden de tres tareas importa: escribir el drop-in → daemon_reload directamente (no como handler) → arrancar el servicio. Un handler se ejecutaría al final del play – en un servidor nuevo, MariaDB arrancaría primero sin el sandbox. Después, wait_for espera a que aparezca el archivo de socket, la señal de que MariaDB acepta conexiones.
Configuración, y un directorio de logs que nadie creó
Un archivo de configuración en /etc/my.cnf.d/ fija lo esencial: escuchar en todas las interfaces (los pods se conectan por la dirección del host; el cortafuegos limita el puerto 3306 a la red de pods), el buffer pool de InnoDB (la caché de páginas de la base de datos, dimensionada por host en el inventario – alrededor de una cuarta parte de la RAM), el comportamiento de escritura, los límites de conexiones, el log de consultas lentas y skip_name_resolve. Esto último evita que MariaDB busque un nombre de host para cada conexión de un pod – una búsqueda que nunca puede tener éxito para una dirección de pod y solo cuesta tiempo.
En las tareas de alrededor se esconden dos historias. El log de consultas lentas necesita un directorio que MariaDB no crea. Sin él, MariaDB registra un error al arrancar y después funciona con ese log desactivado en silencio – un diagnóstico que parece activado y no recoge nada. Así que el rol lo crea, con el tipo SELinux correcto. Y la rotación de logs: el archivo de logrotate del paquete y otro adicional para el log lento cubrían los mismos archivos, y logrotate fallaba cada noche con «duplicate log entry». Ahora el rol gestiona él mismo el archivo del paquete (está marcado como archivo de configuración, así que las actualizaciones del paquete dejan un .rpmnew al lado en lugar de sobrescribirlo) y usa copytruncate, que no necesita iniciar sesión en la base de datos.
Un aviso del rol merece citarse para quien lo edite: las líneas de comentario dentro del content: del archivo de configuración forman parte del archivo en el servidor. Cambiar un comentario ahí cambia el archivo, y el handler reinicia MariaDB.
Solo en la primera ejecución – y en cada ejecución
- name: MariaDB | Check if the Nextcloud database exists
ansible.builtin.shell: |
set -o pipefail
mysql --protocol=socket --socket="{{ mariadb_socket }}" -u root -p"..." \
-e "SHOW DATABASES LIKE '{{ nextcloud_db }}';" | grep -Fx "{{ nextcloud_db }}"
register: db_check
failed_when: false
changed_when: false
no_log: true
- name: MariaDB | Create Nextcloud database user (host restricted to K3s pod CIDR)
community.mysql.mysql_user:
name: "{{ nextcloud_user }}"
host: "{{ k3s_pod_cidr.split('.')[:2] | join('.') }}.%"
password: "{{ nextcloud_pass }}"
priv: "{{ nextcloud_db }}.*:ALL"
when: db_check.rc != 0
no_log: trueLa primera tarea decide si se trata de un servidor nuevo: ¿existe la base de datos de Nextcloud? grep -Fx compara la línea completa de forma literal, así que «nextcloud» no coincide con «nextcloud_old». Si no existe, cuatro tareas con when: db_check.rc != 0 fijan la contraseña de root, escriben un /root/.my.cnf para root, crean la base de datos y el usuario. El usuario solo se permite desde la red de pods: k3s_pod_cidr.split('.')[:2] | join('.') convierte 10.42.0.0/16 en 10.42, y 10.42.% es el comodín de MariaDB para «cualquier dirección que empiece así». Incluso con un error en el cortafuegos, la base de datos rechazaría ese inicio de sesión desde cualquier otro sitio.
Estas tareas usan los módulos de community.mysql, que hablan con MariaDB mediante Python y son idempotentes: «present» crea o corrige, «absent» elimina. Se conectan por el archivo de socket local (login_unix_socket) – y un pequeño set_fact con un if/else en línea elige cuál de las dos rutas de socket habituales existe en este host.
Otras tareas se ejecutan en cada play: eliminar las cuentas anónimas y la base de datos de prueba que trae MariaDB (lo que haría mysql_secure_installation; algunas actualizaciones las recrean) y mantener el usuario de solo lectura del exporter. Ese usuario compartía antes la contraseña de root, lo que significaba que el archivo de configuración del exporter contenía en la práctica las credenciales de root. Ahora tiene la suya. Su lista de privilegios encierra una pequeña lección de idempotencia: MariaDB informa del privilegio REPLICATION CLIENT con su nombre nuevo, BINLOG MONITOR, y mientras el rol pedía el nombre antiguo, el módulo veía una diferencia y lo volvía a conceder en cada ejecución.
⚡ next_redis: tres líneas y una guarda
Redis lo instala el rol de paquetes; este rol solo edita /etc/redis/redis.conf con tres tareas lineinfile – escuchar en localhost y en la dirección pública del host (para que los pods puedan llegar), protected mode activado y una contraseña. backup: true guarda una copia con marca de tiempo del archivo cada vez que cambia una línea.
La línea de la contraseña tiene el mejor comentario. Redis escucha en la dirección pública; solo una regla del cortafuegos lo separaba de internet. «Que una regla del cortafuegos falle en silencio no es hipotético aquí» – el comentario se refiere a que fail2ban estuvo muerto dos días en el blog sin que nadie se diera cuenta. Un Redis sin autenticación que guarda los bloqueos de archivos y las sesiones de Nextcloud no es algo que se deje con una sola capa. La misma contraseña llega a Nextcloud a través del Secret de Kubernetes, y un assert al principio del rol se niega a ejecutarse si el vault no la tiene.
- name: Redis | Flush handlers to apply config changes now
ansible.builtin.meta: flush_handlers
- name: Redis | Wait for Redis to be ready on host IP
ansible.builtin.wait_for:
host: "{{ nextcloud_ipv4 }}"
port: 6379
timeout: 30De nuevo flush_handlers en lugar de esperar al final del play: el rol de configuración, más adelante en el mismo play, habla con Redis a través de los pods. Si Redis solo se reiniciara al final, seguiría escuchando únicamente en localhost cuando se ejecutan esos comandos. wait_for comprueba después que el puerto acepta de verdad conexiones en la dirección que usan los pods.
🚀 next_k3s_deploy: Nextcloud, Collabora y push como objetos
La misma estructura que el rol de despliegue del blog: un archivo de tareas por componente en orden de dependencias, cada uno aplicado con el patrón de aplicación K8s. Una diferencia de comportamiento: aquí las esperas solo se ejecutan when: <apply>.changed – una ejecución sin cambios nunca espera a un despliegue.
Servicios externos: un nombre para algo fuera del clúster
¿Cómo llega un pod a una base de datos que no está en el clúster? Con un Service sin selector de pods y un objeto Endpoints rellenado a mano:
apiVersion: v1
kind: Service
metadata:
name: mariadb
namespace: nextcloud
spec:
ports:
- port: 3306
---
apiVersion: v1
kind: Endpoints
metadata:
name: mariadb # same name = belongs to the Service above
namespace: nextcloud
subsets:
- addresses:
- ip: <the host's address>
ports:
- port: 3306Normalmente Kubernetes rellena él mismo los Endpoints con las direcciones de los pods que coinciden. Sin selector, nos lo deja a nosotros, y ponemos ahí la dirección del host. Dentro del clúster, mariadb y redis son ahora nombres DNS corrientes; que lleven fuera del clúster hasta el host es invisible para Nextcloud. El mismo truco le da a Grafana en el host un nombre al que el Ingress puede enrutar /grafana/.
El pod de Nextcloud
La plantilla del despliegue sigue el patrón del blog – contenedor de inicialización para los propietarios de los directorios, tres contenedores, capabilities retiradas con un comentario por capability, suma de comprobación de la configuración en una anotación para que un ConfigMap modificado redespliegue el pod, sondas de startup/readiness/liveness. Las diferencias dicen algo de Nextcloud:
- Instalación a partir de variables de entorno. En el primer arranque contra una base de datos vacía, la imagen instala Nextcloud a partir de variables
MYSQL_*,NEXTCLOUD_ADMIN_*yREDIS_*. Las contraseñas vuelven a llegar como variables*_FILEque apuntan a archivos de Secret montados – la imagen lo admite. Conviene saberlo para la operación: los valores de base de datos y administrador solo se leen durante esa primerísima instalación. Después, las credenciales viven enconfig.php, y cambiar solo el Secret no cambia nada. - Una ventana de arranque de diez minutos. En un salto de versión mayor, el entrypoint ejecuta la actualización antes de que arranque PHP-FPM, y eso puede tardar minutos. La sonda de arranque permite hasta sesenta intentos con diez segundos de intervalo; sin ella, la sonda liveness mataría el contenedor en mitad de su propia actualización.
- Tres números que deben coincidir. Las subidas de hasta 2 GB necesitan
PHP_UPLOAD_LIMIT, elclient_max_body_sizede nginx en el sidecar y el mismo límite en el Ingress – y el límite de memoria de PHP debe estar por encima del de subida. La plantilla lo dice donde se fijan los valores, porque cambiar solo uno de los tres produce errores que apuntan a otro sitio. - La configuración como archivo. Nextcloud carga cada archivo
*.phpde su directorio de configuración. El rol montacustom.config.phpdesde un ConfigMap junto alconfig.phppropio de Nextcloud: idioma y región, duración de la sesión, la caché y el bloqueo, ajustes de vistas previas, nivel de log, el proxy de confianza (la red de pods, para que Nextcloud vea la dirección real del visitante), 180 días de retención en la papelera. Un valor fijado en este archivo tiene prioridad sobre la misma clave enconfig.php, así que no puede desviarse a través de la interfaz web. - Cachés dimensionadas para Nextcloud. OPcache recibe 256 MB para hasta 20.000 archivos – una instalación con sus apps tiene entre 15.500 y 17.800 archivos PHP, y el antiguo límite de 10.000 hacía que algunos archivos salieran de la caché. APCu, la caché por pod que comparten todos los workers, recibe 128 MB en lugar de los 32 MB de PHP. El número de workers de PHP-FPM se fija por host: 30 en la instancia grande y 20 en la pequeña (los picos medidos fueron 17 de 20 y 10 de 12), cada uno con un límite de contenedor adecuado.
Collabora y una clave que tiene que existir una vez
Collabora firma cada petición WOPI con una proof key; Nextcloud verifica la firma. Las imágenes antiguas generaban esa clave en un script de arranque. La imagen actual arranca su servidor directamente y registraba «Could not open proof RSA key» – funcionaba, pero Nextcloud ya no podía comprobar que las peticiones WOPI vinieran realmente de este Collabora. El rol ahora genera la clave una vez y la guarda como Secret:
- name: Collabora | Does the WOPI proof key secret exist?
kubernetes.core.k8s_info:
kind: Secret
namespace: nextcloud
name: collabora-proof-key
register: _collabora_proof_key
no_log: true
- name: Collabora | Generate the WOPI proof key (first run only)
ansible.builtin.shell: |
set -o pipefail
openssl genrsa -traditional 2048 2>/dev/null \
| k3s kubectl -n nextcloud create secret generic collabora-proof-key --from-file=proof_key=/dev/stdin
when: _collabora_proof_key.resources | length == 0
no_log: trueLa gracia está en la tubería: openssl escribe la clave en su salida, kubectl la lee de /dev/stdin y la mete en el Secret. La clave privada nunca toca el disco del servidor ni aparece en un log. El pod la monta, y fsGroup la hace legible para el usuario de Collabora sin hacerla legible para todos. Tras un cambio en Collabora, next_config le dice a Nextcloud que vuelva a leer el documento de descubrimiento de Collabora, para que la clave nueva se use de inmediato.
Dos notas honestas de la plantilla de Collabora conviene conocerlas como operador. Primero, es el único contenedor que no retira todas las capabilities: Collabora construye su propio sandbox por documento con chroot y namespaces y necesita capabilities que los demás no necesitan. Adivinar ese conjunto a partir de la documentación podría romper la edición de documentos de una forma que solo se nota cuando un usuario abre un archivo, así que se deja así hasta poder probarlo como es debido. Segundo, las sondas: readiness y liveness consultan / (dos bytes, «OK»), no el documento de descubrimiento de 43 KB. El kubelet solo lee los primeros 10 KB de la respuesta de una sonda y cierra la conexión – y Collabora registraba cada uno de esos cierres como dos errores, unas 1.300 líneas cada dos días por servidor. Solo la sonda de arranque sigue usando el descubrimiento, una vez, porque demuestra que todo el servicio está en marcha.
NetworkPolicy: Collabora es la pieza arriesgada
La regla del blog protege la base de datos de todo salvo WordPress. La de Nextcloud tiene otro objetivo: Collabora procesa documentos arbitrarios que suben los usuarios – un punto de entrada más plausible para un atacante que la mayor parte del código PHP –, y no tiene ningún motivo para hablar nunca con MariaDB o Redis. Así que la regla permite a Collabora exactamente lo que se ha comprobado que necesita: peticiones del Ingress y del pod de Nextcloud, DNS y las llamadas de vuelta WOPI a Nextcloud – el puerto 443 en la dirección del host, y explícitamente no el 3306 ni el 6379 en esa misma dirección. Si Collabora quedara comprometido, la base de datos seguiría fuera de su alcance.
Ingress: un host, cuatro minions
La misma estructura master/minion que en el blog, con más minions: las rutas de Nextcloud (con las cabeceras de seguridad), Grafana y – si push está activado – /push/, con cabeceras WebSocket y un tiempo límite largo. El minion de push reescribe /push/x como /x, porque notify_push sirve sus endpoints en su raíz, y su nombre importa: el controlador ordena las locations regex de los minions por el nombre del Ingress, y con el nombre equivocado respondía primero la ruta comodín de Nextcloud («404 para /push/test/cookie» en el primer despliegue). Collabora tiene su propio master para su propio nombre de host.
El CronJob de segundo plano
Cada cinco minutos un pod ejecuta cron.php como www-data, con los mismos volúmenes que Nextcloud. Dos detalles: conserva las capabilities SETUID/SETGID porque usa runuser para pasar de root a www-data, lo que falla sin ellas; y recibe la contraseña de Redis, porque las tareas de fondo toman los mismos bloqueos de archivos que los workers web.
apps.yml: solo las diferencias
Qué apps están activadas o no debe ser reproducible – pero no como lista completa. Las apps incluidas pertenecen a la imagen y cambian entre versiones mayores. Así que el inventario solo enumera las diferencias deliberadas: nextcloud_apps_disabled (desactivadas a la fuerza y mantenidas así a través de las actualizaciones) y nextcloud_apps_enabled (activadas – si es posible).
- name: Apps | Ensure required apps are enabled (only if compatible with this major)
ansible.builtin.command: >-
k3s kubectl exec -n nextcloud {{ nc_pod.stdout }} -c nextcloud-fpm --
php occ app:enable {{ item }}
loop: "{{ nextcloud_apps_enabled }}"
register: nc_app_enable
changed_when: "'already enabled' not in (nc_app_enable.stdout | lower)"
failed_when: false
- name: Apps | Report apps left disabled (no version compatible with this major)
ansible.builtin.debug:
msg: "App '{{ item.item }}' could not be enabled and stays disabled ..."
loop: "{{ nc_app_enable.results | default([]) }}"
when:
- item.rc is defined
- item.rc != 0Tras una actualización mayor, puede que una app aún no tenga una versión compatible. El actualizador del propio Nextcloud simplemente la deja desactivada, y este rol hace lo mismo: failed_when: false al activarla, y luego una segunda tarea recorre los resultados e informa exactamente de qué apps se quedaron desactivadas. Una ejecución que de otro modo fallaría por una app ahora termina y te dice dónde mirar. (occ se ejecuta aquí como root: desde Nextcloud 34, el script occ cambia por sí mismo al propietario de config.php. El rol de configuración sigue usando explícitamente runuser -u www-data.)
notify_push.yml: binario y app al mismo paso
El servicio push se activa por host (nextcloud_notify_push_enabled). El binario viene dentro de la app notify_push, así que el orden es: instalar y activar la app, leer su versión, desplegar el pod para exactamente esa versión, ejecutar la autoprueba. Dos técnicas lo hacen robusto:
- La versión como anotación. Tras una actualización de la app, el proceso en marcha sigue teniendo el binario antiguo. La versión instalada de la app va como anotación a la plantilla del pod – una versión nueva cambia la plantilla, y Kubernetes redespliega el pod. Binario y app nunca pueden desincronizarse.
- Un bucle que sabe de
when. Ansible evalúa elloopde una tarea antes que suwhen. La plantilla necesita la versión de la app, que no existe cuando push está desactivado – así que generarla fallaría aunque la tarea esté a punto de omitirse. Por eso el bucle devuelve una lista vacía cuando push está desactivado:{{ [] if not (nextcloud_notify_push_enabled | bool) else ... }}. Un detalle que solo se aprende al tropezar con él.
El último paso tenía su propio fallo de idempotencia. occ notify_push:setup guarda el endpoint e imprime «configuration saved» cada vez, así que informaba de un cambio en cada ejecución. Ahora el rol lee primero el endpoint guardado: si ya es correcto, solo ejecuta notify_push:self-test – las mismas comprobaciones, sin escribir nada. Un setup fallido, por cierto, es inofensivo: Nextcloud solo pasa los clientes a push cuando la autoprueba ha salido bien; hasta entonces siguen preguntando periódicamente.
| Pieza | Dónde | Qué llevarse |
|---|---|---|
daemon_reload como tarea, no como handler | next_mariadb | Cuando la tarea siguiente lo necesita |
grep -Fx + when: db_check.rc != 0 | next_mariadb | Una configuración única sin bloque |
split / join sobre un CIDR | next_mariadb | Derivar un patrón de host de MariaDB a partir del inventario |
módulos de community.mysql | next_mariadb | Usuarios y bases de datos idempotentes |
lineinfile backup: true | next_redis | Conservar la versión anterior de un archivo de configuración |
flush_handlers + wait_for | next_redis | Reiniciar ya y demostrar que escucha |
| Service sin selector + Endpoints | servicios externos | Dar a un servicio del host un nombre en el clúster |
openssl ... | kubectl create secret --from-file=/dev/stdin | collabora.yml | Crear un secreto que nunca toca el disco |
failed_when: false + un informe sobre results | apps.yml | Tolerar, pero avisar |
| anotación de versión | notify-push | Redesplegar un pod cuando cambia algo fuera de su imagen |
bucle evaluado antes que when | notify_push.yml | Hacer seguro el propio bucle cuando la tarea se omite |
⚙️ next_config: leer, comparar, fijar
Nextcloud está en marcha; ahora sus ajustes. La herramienta es occ, la línea de comandos de Nextcloud, ejecutada dentro del pod como propietario de config.php:
k3s kubectl exec deployment/nextcloud -n nextcloud -c nextcloud-fpm \
-- runuser -u www-data -- php /var/www/html/occ <command>El rol guarda la primera parte una vez con set_fact (occ_exec) y la usa en cada tarea. Después aplica a cada valor un patrón – el que su cabecera llama «get, compare, set»: leer el valor actual con occ config:...:get y registrarlo; escribir solo when difiere. Una ejecución que no cambia nada no informa de nada, y un valor que alguien cambió en la interfaz de administración se restablece y aparece como cambio. Esa segunda parte es lo que hace que la comprobación de deriva tenga sentido para los ajustes de la aplicación, no solo para los archivos.
nc_config.yml: dominios, URL y correo
Los dominios de confianza y la URL para la línea de comandos son simples parejas de leer/comparar/fijar. Los ajustes de correo son más interesantes, porque son nueve y uno es una contraseña:
- name: Config | Read current mail settings
ansible.builtin.command: "{{ occ_exec }} ... config:system:get {{ item.key }}"
loop: "{{ nc_mail_settings }}"
register: nc_mail_current
changed_when: false
failed_when: false # rc 1 = not set yet
- name: Config | Set mail settings that differ
ansible.builtin.command:
argv: "{{ ... + ['config:system:set', item.item.key, '--value', item.item.value] }}"
loop: "{{ nc_mail_current.results }}"
when: item.rc != 0 or (item.stdout | trim) != (item.item.value | string)
no_log: true
- name: Config | Report mail settings a real run would change (--check only)
ansible.builtin.debug: ...
when: ansible_check_mode and (...same condition...)
changed_when: trueTres ideas en un mismo sitio. La tarea de escritura recorre los resultados de la de lectura, así que cada elemento lleva tanto el valor deseado (item.item.value) como el actual (item.stdout). argv pasa los valores literalmente – unas comillas o un espacio en la contraseña no pueden romper la línea de comandos. Y como las tareas command se omiten en una simulación, el cambio pendiente sería invisible ahí; la tercera tarea existe solo para el modo check e informa de lo que cambiaría una ejecución real. La versión antigua reescribía los nueve valores en cada ejecución e informaba «ok» de todos modos, así que una contraseña SMTP cambiada nunca llegaba a verse.
db_maintenance.yml, apps.yml, wopi.yml
- Mantenimiento de la base de datos:
occ db:add-missing-indicesyocc maintenance:repair --include-expensive– las dos cosas de las que, si no, se queja la vista general de administración de Nextcloud tras una actualización. Ambas comprueban qué falta y no hacen nada cuando todo está en su sitio. - Ofimática y vistas previas: Nextcloud Office (la app
richdocuments) se descarga y descomprime con los propios módulos de Ansible en lugar deocc app:install, porque este último truncaba de forma reproducible un archivo de esta app, siempre en el mismo byte. Esquivarocctambién esquiva su comprobación de firma – así que el rol la recupera conocc integrity:check-appy el certificado que trae la app, y avisa si no coincide. La app Preview Generator se instala y activa de forma normal. - WOPI: la lista de direcciones que pueden llamar al endpoint WOPI de Nextcloud (debe contener la red de pods, porque Collabora devuelve las llamadas desde la dirección de su pod) y la URL de Collabora – leer, comparar, fijar. Una tarea usa una variable registrada por
next_k3s_deploy, un rol anterior del mismo play: las variables registradas siguen disponibles para todos los roles posteriores en ese host.collabora_apply is definedprotege las ejecuciones en las que ese rol se omitió con etiquetas.
password_policy.yml: un bloqueo y una lección
El último archivo activa la app de política de contraseñas y fija sus reglas: al menos 12 caracteres, nada de contraseñas comunes y una comprobación contra contraseñas filtradas conocidas. Esa comprobación usa el servicio Have I Been Pwned con k-anonimato: solo los cinco primeros caracteres del hash de la contraseña salen del servidor, y el servicio devuelve todos los hashes filtrados con ese prefijo para compararlos localmente – la contraseña en sí nunca se envía. Las reglas de clases de caracteres (mayúsculas, dígitos, caracteres especiales) están explícitamente desactivadas: la longitud y la comprobación de filtraciones importan más, y las reglas de clases producen sobre todo «Password1!».
Este archivo tiene además el incidente más instructivo de todo el repositorio. Cuando se activó la app de la política en un servidor, cada usuario cuya contraseña tenía más de 300 días quedó bloqueado en el siguiente inicio de sesión – administrador incluido. La causa era un ajuste que quedaba del servidor antiguo, expiration = 300, que había estado en la base de datos sin que nadie lo notara hasta que se activó la app que lo lee. Desde entonces el rol fija explícitamente cada clave de la política, también las que deben estar desactivadas (expiration = 0), para que ningún resto pueda volver. La lección general para operadores: antes de activar una app, lee qué configuración tiene ya.
Un segundo detalle es técnico: Nextcloud 34 guarda los ajustes de las apps con un tipo, y los valores antiguos de versiones anteriores no lo tienen. Fijar una clave así con --type=boolean se detiene en una pregunta interactiva «type yes to confirm» – lo que aborta una ejecución de Ansible. Así que un valor antiguo distinto primero se borra y después se escribe de nuevo con su tipo.
✅ nextcloud_verify
La misma estructura que en el blog (parte 3): comprobaciones que solo recogen, un informe, un correo y un assert al final; lo básico de host, Kubernetes y HTTP viene de common_verify. Las comprobaciones propias de Nextcloud: MariaDB y Redis en marcha en el host, todos los pods y Collabora en marcha, status.php sano, la página de inicio de sesión y el descubrimiento de Collabora accesibles, config.php y el directorio de datos no accesibles desde fuera, tareas de fondo en modo cron, Redis y MariaDB accesibles desde el pod de Nextcloud y la URL ofimática bien configurada.
🔄 nextcloud-update.yml
La parte 1 presentó sus barandillas; aquí está la secuencia completa, porque actualizar una nube de archivos de la que dependen personas merece cuidado:
- Negarse sin
--limit– una instancia cada vez. - Negarse ante una instancia en mal estado: instalada, no en modo mantenimiento, sin actualización de base de datos pendiente. Por qué existe esa guarda.
- Imágenes reconstruidas:
common_image_refreshencuentra los Deployments cuya etiqueta de imagen se reconstruyó en origen; solo esos se reinician, en un orden fijo. occ upgrade– normalmente lo hace el entrypoint al arrancar, se vuelve a llamar como red de seguridad. Solo «Update successful» en la salida cuenta como cambio.occ app:update --all– actualizaciones de apps.- Un segundo reinicio, y este merece una explicación. Nextcloud se ejecuta con
opcache.validate_timestamps=0: PHP nunca comprueba si un archivo cambió, el código compilado se queda en memoria durante toda la vida del pod. Eso es rápido – y significa que los archivos sobrescritos porocc upgradey las actualizaciones de apps seguirían ejecutándose como código antiguo hasta el siguiente reinicio sin relación, correcciones de seguridad incluidas. Así que el pod se reinicia una vez más tras las actualizaciones. - Comprobación posterior: las mismas condiciones de salud que antes. La ejecución falla si Nextcloud no está sano después.
Los pasos 3 a 7 están dentro de un block. El rescue muestra el estado, los pods y eventos, y las últimas líneas de nextcloud.log. Después toma una decisión prudente: sale del modo mantenimiento solo si la actualización en sí ha terminado (sin actualización de base de datos pendiente). Con una actualización pendiente, el modo mantenimiento sigue activado – de lo contrario los usuarios llegarían a una instancia a medio actualizar. Luego se detiene e imprime qué hacer a continuación.
🛠️ En operación: Nextcloud en el día a día
Echar un vistazo
# Pods: nextcloud, collabora, notify-push; cron pods come and go
k3s kubectl get pods -n nextcloud
# Nextcloud's own view: version, maintenance mode, pending upgrade
k3s kubectl exec -n nextcloud deployment/nextcloud -c nextcloud-fpm -- \
runuser -u www-data -- php occ status
# Nextcloud's log (warnings and errors), on the host
tail -f /data/nextcloud.log
# Background jobs: when did cron last run? (Admin settings → Basic settings shows it too)
k3s kubectl get jobs -n nextcloud
# Disk: user data, trash, versions
df -h /dataLa rutina
| Cuándo | Qué | Cómo |
|---|---|---|
| Cuando hay actualizaciones disponibles | Imágenes reconstruidas, actualizaciones de apps, versiones de parche | ansible-playbook nextcloud-update.yml --limit <host>, primero --check |
| Para una versión mayor nueva | Cambiar la etiqueta de imagen en versions.yml – una versión mayor cada vez | leer las notas de la versión, tomar un snapshot, luego ansible-playbook nextcloud-k3s.yml --limit <host> (el entrypoint ejecuta la actualización cuando arranca la imagen nueva) |
| Tras cada ejecución | Leer el correo de verificación | todo en verde, o los nombres de las comprobaciones fallidas |
| De vez en cuando | Mirar la vista general de administración | Ajustes de administración → Vista general lista los avisos que ha encontrado el propio Nextcloud |
Cuando algo va mal
- «No se pudo cargar la ofimática», y al reintentar funciona: un hipo pasajero entre navegador, Collabora y Nextcloud – normalmente inofensivo. Si pasa a menudo, Collabora necesita más procesos prearrancados o más memoria (
collabora_num_preallocy el límite de memoria, por host en el inventario). La instancia grande mantiene 10 procesos precalentados en 8 GiB, la pequeña 6 en 4 GiB. - Nextcloud atascado en modo mantenimiento tras una actualización fallida: mira
occ status. Si hay una actualización pendiente, ejecutaocc upgradey lee su salida; solo entoncesocc maintenance:mode --off. Nunca lo desactives con una actualización todavía pendiente. - Usuarios bloqueados por la política de contraseñas: comprueba
occ config:app:get password_policy expiration– debe ser 0. El rol lo fija, pero es lo primero que hay que mirar. - El disco se llena: la alarma llega al 80 por ciento (parte 6). La papelera guarda los archivos borrados 180 días y se limpia sola; aun así, la papelera de un solo usuario puede ser grande. Las vistas previas pueden regenerarse y no son una reserva real.
occ trashbin:cleanup --all-usersvacía todas las papeleras – con el consentimiento de los usuarios, no por sorpresa. - Todo el servidor ha desaparecido: monta uno nuevo con el playbook completo, luego restaura el volcado de la base de datos, el directorio de la aplicación (con
config.phpy las apps instaladas) y los datos de los usuarios desde la copia externa con el script de restauración, y vuelve a ejecutar el playbook una vez. El manual de operación del repositorio enumera cada paso.
🧭 Lo que viene
Con la parte 4 quedan cubiertas ambas aplicaciones. La parte 5 reúne todos los roles de seguridad en un mismo sitio – SSH, el cortafuegos, el endurecimiento CIS, SELinux, la auditoría, AIDE, rkhunter, ClamAV y fail2ban – y explica contra qué protege cada capa. La parte 6 es la monitorización: exporters, Prometheus, Grafana y el perro guardián Monit, y qué significa cada alarma.

