
🌐 Auch auf: English · Français · Español
Teil 4 der Ansible-Tour. Teil 1 hat das Gerüst erklärt, Teil 2 den Server und die Plattform, Teil 3 den Blog. Dieser Teil ist die zweite Anwendung: Nextcloud. Er beschreibt das Release v3.3.0. Wer Teil 3 gelesen hat, wird vieles wiedererkennen – ich weise auf die Unterschiede hin und erkläre, was neu ist, statt die gemeinsamen Muster zu wiederholen.
Nextcloud ist eine Datei-Cloud: Dateien, Kalender, Kontakte, geteilte Ordner und – mit Collabora – Dokumente, die man gemeinsam im Browser bearbeitet. Zwei der drei Server hier betreiben sie für Gruppen sehr unterschiedlicher Größe, mit demselben Code und anderen Werten im Inventory. Die Arbeit verteilt sich auf drei Rollen plus das Update-Playbook:
| Rolle / Playbook | Was sie tut |
|---|---|
next_mariadb, next_redis | Datenbank und Cache, direkt auf dem Host |
next_k3s_deploy | Nextcloud, Collabora, der Push-Dienst und alles drumherum als Kubernetes-Objekte |
next_config | Die eigenen Einstellungen von Nextcloud, gesetzt mit occ – lesen, vergleichen, nur schreiben, was abweicht |
nextcloud_verify | Smoke-Tests am Ende des Laufs |
nextcloud-update.yml | Das Routine-Update, mit Zustandsprüfungen davor und danach und einem festgelegten Weg für den Fehlerfall |
🏗️ Was läuft, und warum
Der Weg einer Anfrage ist derselbe wie beim Blog: Ingress-Controller (TLS) → nginx-Sidecar → PHP-FPM, in einem Pod. Anders ist alles drumherum.
| Komponente | Warum sie da ist |
|---|---|
| Nextcloud (PHP-FPM + nginx) | Die Anwendung selbst, aus dem offiziellen Image nextcloud:fpm, mit nginx als Sidecar für statische Dateien – dieselbe Begründung wie bei WordPress. Nextcloud zu aktualisieren heißt, das Image zu tauschen; kein PHP auf dem Host. |
| Zwei Host-Verzeichnisse | Die Anwendungsdateien (/srv/nextcloud-www) und die Nutzerdaten (/data) sind bewusst getrennt. Ein neues Image ersetzt die Anwendungsdateien; die Dateien der Nutzer bleiben, wo sie sind, und lassen sich unabhängig sichern, und eine frische Installation braucht nur ein leeres Anwendungsverzeichnis. |
| MariaDB auf dem Host | Anders als beim Blog läuft die Datenbank als normaler Dienst auf dem Host. Ihre Datendateien liegen direkt im Dateisystem des Hosts, ohne eine Volume-Schicht eines Containers dazwischen – für eine Datenbank dieser Größe und Wichtigkeit eine Sache weniger zwischen Daten und Platte. Die Pods erreichen sie über einen Kubernetes-Service, der auf den Host zeigt (unten erklärt). |
| Redis auf dem Host | Nextcloud nutzt Redis für zwei Dinge: einen Cache, den sich alle PHP-Prozesse teilen, und File Locking – damit zwei Clients, die im selben Moment dieselbe Datei speichern, sie nicht beschädigen. Locking ist für eine Datei-Cloud mit Sync-Clients unverzichtbar. Ein Cache pro Prozess (APCu) erledigt die kleinen lokalen Abfragen; Redis alles, was über Prozesse hinweg konsistent sein muss. |
| Collabora Online (CODE) | Eine Office-Suite, die auf dem Server läuft und im Browser angezeigt wird. Nextcloud und Collabora sprechen über WOPI, ein Protokoll, in dem der Editor den Dateiserver bittet „gib mir diese Datei“ und „speichere diese Version“, signiert mit einem Schlüssel, damit der Dateiserver weiß, dass die Anfragen echt sind. Collabora läuft in einem eigenen Pod, hinter einem eigenen Hostnamen. |
| notify_push | Ohne ihn fragt jeder Desktop- und Mobil-Client alle dreißig Sekunden beim Server nach, ob sich etwas geändert hat. notify_push hält eine WebSocket-Verbindung offen und sagt den Clients Bescheid, wenn sich eine Datei ändert – weniger Last, schnellerer Abgleich. Es ist ein kleines Programm, das in einer Nextcloud-App mitkommt und als eigener Pod läuft. |
| Ein CronJob für Hintergrundaufgaben | Nextcloud hat eine Warteschlange für Hintergrundarbeit: Vorschaubilder, Aufräumen, Benachrichtigungen, Ablauf alter Versionen. Ein CronJob führt alle fünf Minuten cron.php aus – der Modus, den Nextcloud selbst gegenüber „beim Seitenaufruf“ empfiehlt. |
| Vorschaubilder | Miniaturen für Bilder, Videos und Dokumente, im Hintergrund von der App Preview Generator erzeugt statt beim ersten Klick. |
Der Hintergrund zu diesem Setup verteilt sich auf ältere Beiträge: wie die Instanzen vor K3s betrieben wurden und die Migration auf K3s, Schritt für Schritt.
🗄️ next_mariadb: eine Datenbank auf dem Host
Die Rolle installiert MariaDB aus der Paketquelle, die next_packages eingerichtet hat (Teil 2), sichert es ab und legt die Nextcloud-Datenbank an. Ihr Kopf nennt die Reihenfolge, und die Reihenfolge erklärt die Rolle.
Sandbox vor dem ersten Start
MariaDB bekommt einen systemd-Drop-in mit Sandbox-Einstellungen (aus dem files/ der Rolle): welche Verzeichnisse es schreiben darf, welche Kernel-Funktionen es nicht anfassen darf. Der Kopfkommentar macht einen Punkt, den man wiederholen sollte: Dieselbe Art von Einschränkung wurde bei SSH bewusst nicht in ihrer strengsten Form angewendet, weil sshd Benutzersitzungen startet und alles, was ein Benutzer oder Ansible tut, sie erben würde. MariaDB startet nichts, also trifft eine Einschränkung nur die Datenbank. Seit dem 16.10.2026 ist ihr /tmp ein eigenes tmpfs mit 4 GB, Temp-Tabellen und Sortierdateien bleiben also im RAM. Mit PrivateTmp=yes darf man das nicht kombinieren: Mit systemd-run getestet, gewinnt diese Einstellung still, und /tmp bleibt auf der Platte.
Die Reihenfolge von drei Tasks ist wichtig: Drop-in schreiben → daemon_reload direkt (nicht als Handler) → Dienst starten. Ein Handler liefe am Ende des Plays – auf einem frischen Server würde MariaDB erst ohne Sandbox starten. Dann wartet wait_for, bis die Socket-Datei erscheint, das Zeichen dafür, dass MariaDB Verbindungen annimmt.
Konfiguration, und ein Log-Verzeichnis, das niemand angelegt hat
Eine Konfigurationsdatei in /etc/my.cnf.d/ setzt das Wesentliche: auf allen Schnittstellen lauschen (die Pods verbinden sich über die Adresse des Hosts; die Firewall beschränkt Port 3306 auf das Pod-Netz), den InnoDB-Buffer-Pool (den Cache für Datenbankseiten, pro Host im Inventory bemessen – etwa ein Viertel des RAM), das Schreibverhalten, Verbindungslimits, das Slow-Query-Log und skip_name_resolve. Letzteres hält MariaDB davon ab, für jede Pod-Verbindung einen Hostnamen nachzuschlagen – eine Abfrage, die für eine Pod-Adresse nie gelingen kann und nur Zeit kostet.
In den Tasks drumherum stecken zwei Geschichten. Das Slow-Query-Log braucht ein Verzeichnis, das MariaDB nicht anlegt. Ohne es protokolliert MariaDB beim Start einen Fehler und läuft dann mit still abgeschaltetem Slow-Log – eine Diagnose, die eingeschaltet aussieht und nichts sammelt. Also legt die Rolle es an, mit dem richtigen SELinux-Typ. Und die Logrotation: Die logrotate-Datei des Pakets und eine zusätzliche für das Slow-Log deckten dieselben Dateien ab, und logrotate scheiterte jede Nacht mit „duplicate log entry“. Jetzt verwaltet die Rolle die Paketdatei selbst (sie ist als Konfigurationsdatei markiert, Paket-Updates legen also eine .rpmnew daneben, statt sie zu überschreiben) und nutzt copytruncate, das keinen Datenbank-Login braucht.
Eine Warnung in der Rolle ist es wert, für alle zitiert zu werden, die sie bearbeiten: Die Kommentarzeilen innerhalb des content: der Konfigurationsdatei sind Teil der Datei auf dem Server. Ändert man dort einen Kommentar, ändert sich die Datei, und der Handler startet MariaDB neu.
Nur beim ersten Lauf – und bei jedem Lauf
- 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: trueDer erste Task entscheidet, ob das ein neuer Server ist: Gibt es die Nextcloud-Datenbank? grep -Fx vergleicht die ganze Zeile wörtlich, „nextcloud“ passt also nicht auf „nextcloud_old“. Existiert sie nicht, setzen vier Tasks mit when: db_check.rc != 0 das root-Passwort, schreiben eine /root/.my.cnf für root, legen Datenbank und Benutzer an. Der Benutzer ist nur aus dem Pod-Netz erlaubt: k3s_pod_cidr.split('.')[:2] | join('.') macht aus 10.42.0.0/16 ein 10.42, und 10.42.% ist der MariaDB-Platzhalter für „jede Adresse, die so beginnt“. Selbst bei einem Firewall-Fehler würde die Datenbank diesen Login von überall sonst ablehnen.
Diese Tasks nutzen die Module von community.mysql, die über Python mit MariaDB sprechen und idempotent sind: „present“ legt an oder korrigiert, „absent“ entfernt. Sie verbinden sich über die lokale Socket-Datei (login_unix_socket) – und ein kleines set_fact mit einem Inline-if/else wählt den der beiden üblichen Socket-Pfade, der auf diesem Host existiert.
Andere Tasks laufen bei jedem Play: die anonymen Konten und die Testdatenbank entfernen, die MariaDB mitbringt (was mysql_secure_installation tun würde; manche Upgrades legen sie neu an), und den nur lesenden Benutzer des Exporters pflegen. Dieser Benutzer teilte sich früher das root-Passwort, was hieß, dass die Konfigurationsdatei des Exporters faktisch die Zugangsdaten von root enthielt. Jetzt hat er ein eigenes. Seine Rechteliste trägt eine kleine Lektion in Idempotenz: MariaDB meldet das Recht REPLICATION CLIENT unter seinem neueren Namen BINLOG MONITOR, und solange die Rolle nach dem alten Namen fragte, sah das Modul einen Unterschied und vergab das Recht bei jedem Lauf neu.
⚡ next_redis: drei Zeilen und eine Wache
Redis installiert die Paket-Rolle; diese Rolle bearbeitet nur /etc/redis/redis.conf mit drei lineinfile-Tasks – an localhost und die öffentliche Adresse des Hosts binden (damit die Pods es erreichen), Protected Mode an und ein Passwort. backup: true behält eine Kopie der Datei mit Zeitstempel, wann immer sich eine Zeile ändert.
Die Passwortzeile hat den besseren Kommentar. Redis lauscht auf der öffentlichen Adresse; nur eine Firewall-Regel stand zwischen ihm und dem Internet. „Dass eine Firewall-Regel still versagt, ist hier keine Theorie“ – der Kommentar bezieht sich darauf, dass fail2ban auf dem Blog zwei Tage tot dalag, ohne dass es jemand bemerkte. Ein Redis ohne Authentifizierung, das die File-Locks und Sitzungen von Nextcloud hält, lässt man nicht auf einer einzigen Schutzschicht. Dasselbe Passwort geht über das Kubernetes-Secret an Nextcloud, und ein assert am Anfang der Rolle verweigert den Lauf, wenn es im Vault fehlt.
- 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: 30Wieder flush_handlers, statt auf das Ende des Plays zu warten: Die Konfigurationsrolle später im selben Play spricht über die Pods mit Redis. Würde Redis erst am Ende neu starten, lauschte es noch nur auf localhost, wenn diese Befehle laufen. wait_for prüft dann, dass der Port auf der Adresse, die die Pods nutzen, wirklich Verbindungen annimmt.
🚀 next_k3s_deploy: Nextcloud, Collabora und Push als Objekte
Derselbe Aufbau wie bei der Deploy-Rolle des Blogs: eine Task-Datei pro Komponente in Abhängigkeitsreihenfolge, jede mit dem K8s-Apply-Muster angewendet. Ein Unterschied im Verhalten: Hier laufen die Wartevorgänge nur when: <apply>.changed – ein unveränderter Lauf wartet nie auf einen Rollout.
Externe Dienste: ein Name für etwas außerhalb des Clusters
Wie erreicht ein Pod eine Datenbank, die nicht im Cluster ist? Mit einem Service ohne Pod-Selektor und einem Endpoints-Objekt, das man von Hand füllt:
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: 3306Normalerweise füllt Kubernetes die Endpoints selbst mit den Adressen passender Pods. Ohne Selektor überlässt es das uns, und wir tragen die Adresse des Hosts ein. Im Cluster sind mariadb und redis jetzt gewöhnliche DNS-Namen; dass sie aus dem Cluster hinaus zum Host führen, sieht Nextcloud nicht. Derselbe Trick gibt Grafana auf dem Host einen Namen, an den der Ingress /grafana/ leiten kann.
Der Nextcloud-Pod
Das Deployment-Template folgt dem Muster des Blogs – Init-Container für die Besitzer der Verzeichnisse, drei Container, Capabilities mit Kommentar pro Capability entzogen, Prüfsumme der Konfiguration in einer Annotation, damit eine geänderte ConfigMap den Pod neu ausrollt, Startup-/Readiness-/Liveness-Probes. Die Unterschiede sagen etwas über Nextcloud:
- Installation aus Umgebungsvariablen. Beim ersten Start gegen eine leere Datenbank installiert das Image Nextcloud aus
MYSQL_*-,NEXTCLOUD_ADMIN_*– undREDIS_*-Variablen. Passwörter kommen wieder als*_FILE-Variablen, die auf eingehängte Secret-Dateien zeigen – das Image unterstützt das. Wichtig für den Betrieb: Die Datenbank- und Admin-Werte werden nur bei dieser allerersten Installation gelesen. Danach liegen die Zugangsdaten inconfig.php, und das Secret allein zu ändern, ändert nichts. - Ein Startfenster von zehn Minuten. Bei einem Sprung der Hauptversion führt der Entrypoint das Upgrade aus, bevor PHP-FPM startet, und das kann Minuten dauern. Die Startup-Probe erlaubt bis zu sechzig Versuche im Abstand von zehn Sekunden; ohne sie würde die Liveness-Probe den Container mitten in seinem eigenen Upgrade beenden.
- Drei Zahlen, die übereinstimmen müssen. Uploads bis 2 GB brauchen
PHP_UPLOAD_LIMIT, dasclient_max_body_sizevon nginx im Sidecar und dasselbe Limit am Ingress – und das Speicherlimit von PHP muss über dem Upload-Limit liegen. Das Template sagt das dort, wo die Werte gesetzt werden, weil eine einzelne geänderte der drei Zahlen Fehler erzeugt, die woandershin zeigen. - Konfiguration als Datei. Nextcloud lädt jede
*.php-Datei in seinem Konfigurationsverzeichnis. Die Rolle hängtcustom.config.phpaus einer ConfigMap neben die eigeneconfig.phpvon Nextcloud: Sprache und Region, Sitzungsdauer, Cache und Locking, Vorschau-Einstellungen, Log-Level, der vertrauenswürdige Proxy (das Pod-Netz, damit Nextcloud die echte Adresse des Besuchers sieht), 180 Tage Aufbewahrung im Papierkorb. Ein in dieser Datei gesetzter Wert hat Vorrang vor demselben Schlüssel inconfig.php, kann also nicht über die Weboberfläche abweichen. - Caches für Nextcloud bemessen. OPcache bekommt 256 MB für bis zu 20.000 Dateien – eine Installation mit ihren Apps hat 15.500 bis 17.800 PHP-Dateien, und das alte Limit von 10.000 ließ Dateien wieder aus dem Cache fallen. APCu, der Cache pro Pod, den sich alle Worker teilen, bekommt 128 MB statt der 32 MB von PHP. Die Zahl der PHP-FPM-Worker steht pro Host im Inventory: 30 auf der großen Instanz und 20 auf der kleinen (gemessene Spitzen waren 17 von 20 und 10 von 12), jeweils mit passendem Container-Limit.
Collabora und ein Schlüssel, der einmal existieren muss
Collabora signiert jede WOPI-Anfrage mit einem Proof Key; Nextcloud prüft die Signatur. Ältere Images erzeugten diesen Schlüssel in einem Startskript. Das aktuelle Image startet seinen Server direkt und protokollierte „Could not open proof RSA key“ – es funktionierte, aber Nextcloud konnte nicht mehr prüfen, ob WOPI-Anfragen wirklich von diesem Collabora kommen. Die Rolle erzeugt den Schlüssel jetzt einmal und legt ihn als Secret ab:
- 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: trueDie Pipe ist der Clou: openssl schreibt den Schlüssel in seine Ausgabe, kubectl liest ihn aus /dev/stdin ins Secret. Der private Schlüssel berührt nie die Platte des Servers und taucht nie in einem Log auf. Der Pod hängt ihn ein, und fsGroup macht ihn für den Benutzer von Collabora lesbar, ohne ihn für alle lesbar zu machen. Nach einer Änderung an Collabora sagt next_config Nextcloud, das Discovery-Dokument von Collabora neu zu lesen, damit der neue Schlüssel sofort genutzt wird.
Zwei ehrliche Anmerkungen im Collabora-Template sollte man als Betreiber kennen. Erstens ist es der eine Container, der nicht alle Capabilities abgibt: Collabora baut pro Dokument eine eigene Sandbox mit chroot und Namespaces und braucht Capabilities, die die anderen nicht brauchen. Diese Menge aus der Dokumentation zu erraten, könnte das Bearbeiten von Dokumenten auf eine Weise kaputtmachen, die sich erst zeigt, wenn ein Nutzer eine Datei öffnet – also bleibt es, bis es sich richtig testen lässt. Zweitens die Probes: Readiness und Liveness fragen / ab (zwei Bytes, „OK“), nicht das 43 KB große Discovery-Dokument. Das kubelet liest nur die ersten 10 KB einer Probe-Antwort und schließt die Verbindung – und Collabora protokollierte jedes solche Schließen als zwei Fehler, etwa 1.300 Zeilen pro zwei Tage und Server. Nur die Startup-Probe nutzt noch Discovery, einmal, weil sie beweist, dass der ganze Dienst läuft.
NetworkPolicy: Collabora ist der riskante Teil
Die Regel des Blogs schützt die Datenbank vor allem außer WordPress. Die Regel von Nextcloud hat ein anderes Ziel: Collabora verarbeitet beliebige Dokumente, die Nutzer hochladen – ein plausiblerer Einstiegspunkt für einen Angreifer als der meiste PHP-Code –, und es hat keinen Grund, jemals mit MariaDB oder Redis zu sprechen. Also erlaubt die Regel Collabora genau das, was nachweislich gebraucht wird: Anfragen vom Ingress und vom Nextcloud-Pod, DNS und die WOPI-Rückrufe an Nextcloud – Port 443 auf die Adresse des Hosts und ausdrücklich nicht 3306 oder 6379 auf derselben Adresse. Wäre Collabora kompromittiert, bliebe die Datenbank trotzdem außer Reichweite.
Ingress: ein Host, vier Minions
Dieselbe Master/Minion-Struktur wie beim Blog, mit mehr Minions: die Routen von Nextcloud (mit den Sicherheits-Headern), Grafana und – wenn Push eingeschaltet ist – /push/, mit WebSocket-Headern und einem langen Zeitlimit. Der Push-Minion schreibt /push/x auf /x um, weil notify_push seine Endpunkte an seiner Wurzel anbietet, und sein Name zählt: Der Controller sortiert die Regex-Locations der Minions nach dem Namen des Ingress, und der falsche Name ließ die Auffangroute von Nextcloud zuerst antworten („404 für /push/test/cookie“ beim ersten Rollout). Collabora bekommt einen eigenen Master für seinen eigenen Hostnamen.
Der CronJob für den Hintergrund
Alle fünf Minuten führt ein Pod cron.php als www-data aus, mit denselben Volumes wie Nextcloud. Zwei Details: Er behält die Capabilities SETUID/SETGID, weil er mit runuser von root zu www-data wechselt, was ohne sie scheitert; und er bekommt das Redis-Passwort, weil Hintergrundaufgaben dieselben File-Locks nehmen wie die Web-Worker.
apps.yml: nur die Abweichungen
Welche Apps an oder aus sind, soll reproduzierbar sein – aber nicht als vollständige Liste. Die mitgelieferten Apps gehören zum Image und ändern sich zwischen Hauptversionen. Also führt das Inventory nur die bewussten Abweichungen: nextcloud_apps_disabled (zwangsweise aus und über Upgrades hinweg aus gehalten) und nextcloud_apps_enabled (eingeschaltet – wenn möglich).
- 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 != 0Nach einem großen Upgrade hat eine App vielleicht noch keine passende Version. Der eigene Upgrader von Nextcloud lässt sie einfach deaktiviert, und diese Rolle tut dasselbe: failed_when: false beim Einschalten, dann läuft ein zweiter Task über die Ergebnisse und meldet genau, welche Apps aus geblieben sind. Ein Lauf, der sonst an einer App scheitern würde, läuft jetzt durch und sagt, wo man hinschauen muss. (occ läuft hier als root: Seit Nextcloud 34 wechselt das Skript occ von selbst zum Besitzer von config.php. Die Konfigurationsrolle nutzt trotzdem ausdrücklich runuser -u www-data.)
notify_push.yml: Binary und App im Gleichschritt
Der Push-Dienst wird pro Host eingeschaltet (nextcloud_notify_push_enabled). Das Binary kommt in der App notify_push mit, also ist die Reihenfolge: App installieren und einschalten, ihre Version lesen, den Pod für genau diese Version ausrollen, den Selbsttest ausführen. Zwei Techniken machen das robust:
- Die Version als Annotation. Nach einem App-Update hat der laufende Prozess noch das alte Binary. Die installierte App-Version kommt als Annotation ins Pod-Template – eine neue Version ändert das Template, und Kubernetes rollt den Pod neu aus. Binary und App können nie auseinanderlaufen.
- Eine Schleife, die von
whenweiß. Ansible wertet dieloopeines Tasks vor seinemwhenaus. Das Template braucht die App-Version, die es nicht gibt, wenn Push abgeschaltet ist – sein Rendern würde also scheitern, obwohl der Task gleich übersprungen wird. Deshalb gibt die Schleife eine leere Liste zurück, wenn Push aus ist:{{ [] if not (nextcloud_notify_push_enabled | bool) else ... }}. Ein Detail, das man erst lernt, wenn man dagegenläuft.
Der letzte Schritt hatte einen eigenen Idempotenz-Fehler. occ notify_push:setup speichert den Endpunkt und gibt jedes Mal „configuration saved“ aus, meldete also bei jedem Lauf eine Änderung. Jetzt liest die Rolle zuerst den gespeicherten Endpunkt: Stimmt er schon, führt sie nur notify_push:self-test aus – dieselben Prüfungen, nichts geschrieben. Ein gescheitertes Setup ist übrigens harmlos: Nextcloud stellt Clients erst auf Push um, wenn der Selbsttest bestanden ist; bis dahin fragen sie weiter regelmäßig nach.
| Baustein | Wo | Was man mitnimmt |
|---|---|---|
daemon_reload als Task, nicht als Handler | next_mariadb | Wenn gleich der nächste Task es braucht |
grep -Fx + when: db_check.rc != 0 | next_mariadb | Eine einmalige Einrichtung ohne Block |
split / join auf einem CIDR | next_mariadb | Ein MariaDB-Host-Muster aus dem Inventory ableiten |
Module von community.mysql | next_mariadb | Idempotente Benutzer und Datenbanken |
lineinfile backup: true | next_redis | Die vorherige Fassung einer Konfigurationsdatei behalten |
flush_handlers + wait_for | next_redis | Jetzt neu starten und beweisen, dass es lauscht |
| Service ohne Selektor + Endpoints | externe Dienste | Einem Host-Dienst einen Namen im Cluster geben |
openssl ... | kubectl create secret --from-file=/dev/stdin | collabora.yml | Ein Secret anlegen, das nie die Platte berührt |
failed_when: false + ein Bericht über results | apps.yml | Tolerieren, aber Bescheid sagen |
| Versions-Annotation | notify-push | Einen Pod neu ausrollen, wenn sich etwas außerhalb seines Images ändert |
Schleife vor when ausgewertet | notify_push.yml | Die Schleife selbst sicher machen, wenn der Task übersprungen wird |
⚙️ next_config: lesen, vergleichen, setzen
Nextcloud läuft; jetzt seine Einstellungen. Das Werkzeug ist occ, die Kommandozeile von Nextcloud, im Pod als Besitzer von config.php ausgeführt:
k3s kubectl exec deployment/nextcloud -n nextcloud -c nextcloud-fpm \
-- runuser -u www-data -- php /var/www/html/occ <command>Die Rolle speichert den ersten Teil einmal mit set_fact (occ_exec) und nutzt ihn in jedem Task. Dann wendet sie auf jeden Wert ein Muster an – das Muster, das ihr Kopf „get, compare, set“ nennt: den aktuellen Wert mit occ config:...:get lesen und registrieren; nur schreiben, when er abweicht. Ein Lauf, der nichts ändert, meldet nichts, und ein Wert, den jemand in der Admin-Oberfläche geändert hat, wird zurückgesetzt und taucht als Änderung auf. Dieser zweite Teil macht den Drift-Check für Anwendungseinstellungen aussagekräftig, nicht nur für Dateien.
nc_config.yml: Domains, URL und Mail
Vertrauenswürdige Domains und die URL für die Kommandozeile sind einfache Paare aus lesen/vergleichen/setzen. Die Mail-Einstellungen sind interessanter, denn es sind neun, und eine davon ist ein Passwort:
- 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: trueDrei Ideen an einer Stelle. Der schreibende Task läuft über die Ergebnisse des lesenden, sodass jedes Element sowohl den gewünschten Wert (item.item.value) als auch den aktuellen (item.stdout) trägt. argv übergibt die Werte wörtlich – ein Anführungszeichen oder Leerzeichen im Passwort kann die Befehlszeile nicht zerbrechen. Und weil command-Tasks im Probelauf übersprungen werden, wäre die anstehende Änderung dort unsichtbar; den dritten Task gibt es nur für den Check-Modus, und er meldet, was ein echter Lauf ändern würde. Die alte Fassung schrieb bei jedem Lauf alle neun Werte neu und meldete so oder so „ok“, ein geändertes SMTP-Passwort tauchte also überhaupt nie auf.
db_maintenance.yml, apps.yml, wopi.yml
- Datenbankwartung:
occ db:add-missing-indicesundocc maintenance:repair --include-expensive– die beiden Dinge, über die die Admin-Übersicht von Nextcloud nach einem Upgrade sonst meckert. Beide prüfen, was fehlt, und tun nichts, wenn alles vorhanden ist. - Office und Vorschaubilder: Nextcloud Office (die App
richdocuments) wird mit Ansibles eigenen Modulen heruntergeladen und entpackt statt mitocc app:install, weil Letzteres reproduzierbar eine Datei dieser App jedes Mal an derselben Byte-Position abschnitt. Weroccumgeht, umgeht auch dessen Signaturprüfung – also holt die Rolle sie mitocc integrity:check-appund dem Zertifikat aus der App nach und warnt bei einer Abweichung. Die App Preview Generator wird normal installiert und eingeschaltet. - WOPI: die Liste der Adressen, die den WOPI-Endpunkt von Nextcloud aufrufen dürfen (sie muss das Pod-Netz enthalten, weil Collabora von seiner Pod-Adresse aus zurückruft), und die Collabora-URL – lesen, vergleichen, setzen. Ein Task nutzt eine Variable, die
next_k3s_deployregistriert hat, eine frühere Rolle im selben Play: Registrierte Variablen bleiben für alle späteren Rollen auf diesem Host verfügbar.collabora_apply is definedschützt Läufe, in denen diese Rolle per Tag übersprungen wurde.
password_policy.yml: eine Aussperrung und eine Lektion
Die letzte Datei schaltet die App für die Passwortrichtlinie ein und setzt ihre Regeln: mindestens 12 Zeichen, keine gängigen Passwörter und ein Abgleich mit bekannten geleakten Passwörtern. Dieser Abgleich nutzt den Dienst Have I Been Pwned mit k-Anonymität: Nur die ersten fünf Zeichen des Passwort-Hashes verlassen den Server, und der Dienst gibt alle geleakten Hashes mit diesem Anfang zurück, die lokal verglichen werden – das Passwort selbst wird nie verschickt. Regeln für Zeichenklassen (Großbuchstaben, Ziffern, Sonderzeichen) sind ausdrücklich aus: Länge und Leak-Abgleich zählen mehr, und Klassenregeln produzieren vor allem „Password1!“.
Diese Datei hat auch den lehrreichsten Vorfall des ganzen Repositorys. Als die Richtlinien-App auf einem Server eingeschaltet wurde, wurde jeder Nutzer, dessen Passwort älter als 300 Tage war, beim nächsten Login ausgesperrt – auch der Admin. Ursache war eine übrig gebliebene Einstellung vom alten Server, expiration = 300, die unbemerkt in der Datenbank gelegen hatte, bis die App eingeschaltet wurde, die sie liest. Seitdem setzt die Rolle jeden Schlüssel der Richtlinie ausdrücklich, auch die, die aus sein sollen (expiration = 0), sodass kein Überbleibsel zurückkommen kann. Die allgemeine Lektion für Betreiber: Bevor du eine App einschaltest, lies, welche Konfiguration sie schon hat.
Ein zweites Detail ist technisch: Nextcloud 34 speichert App-Einstellungen mit einem Typ, und alte Werte aus früheren Versionen sind ohne Typ. Einen solchen Schlüssel mit --type=boolean zu setzen, bleibt an einer interaktiven Abfrage „type yes to confirm“ hängen – was einen Ansible-Lauf abbricht. Also wird ein abweichender alter Wert erst gelöscht und dann mit seinem Typ neu geschrieben.
✅ nextcloud_verify
Derselbe Aufbau wie beim Blog (Teil 3): Prüfungen, die nur sammeln, ein Bericht, eine Mail und ein assert am Ende; die Grundlagen für Host, Kubernetes und HTTP aus common_verify. Die Nextcloud-spezifischen Prüfungen: MariaDB und Redis laufen auf dem Host, alle Pods und Collabora laufen, status.php gesund, die Login-Seite und das Discovery von Collabora erreichbar, config.php und das Datenverzeichnis von außen nicht erreichbar, Hintergrundaufgaben im Cron-Modus, Redis und MariaDB vom Nextcloud-Pod aus erreichbar und die Office-URL richtig konfiguriert.
🔄 nextcloud-update.yml
Teil 1 hat seine Leitplanken vorgestellt; hier ist der ganze Ablauf, denn eine Datei-Cloud zu aktualisieren, auf die Menschen angewiesen sind, verdient Sorgfalt:
- Ohne
--limitverweigern – eine Instanz nach der anderen. - Eine ungesunde Instanz verweigern: installiert, nicht im Wartungsmodus, kein ausstehendes Datenbank-Upgrade. Warum es diese Wache gibt.
- Neu gebaute Images:
common_image_refreshfindet Deployments, deren Image-Tag upstream neu gebaut wurde; nur diese werden neu gestartet, in fester Reihenfolge. occ upgrade– normalerweise erledigt das der Entrypoint beim Start, hier noch einmal als Sicherheitsnetz aufgerufen. Nur „Update successful“ in der Ausgabe zählt als Änderung.occ app:update --all– App-Updates.- Ein zweiter Neustart, und dieser verdient eine Erklärung. Nextcloud läuft mit
opcache.validate_timestamps=0: PHP prüft nie, ob sich eine Datei geändert hat, kompilierter Code bleibt für die Lebensdauer des Pods im Speicher. Das ist schnell – und es heißt, dass Dateien, dieocc upgradeund die App-Updates überschrieben haben, bis zum nächsten unabhängigen Neustart als alter Code weiterlaufen würden, Sicherheitskorrekturen eingeschlossen. Also wird der Pod nach den Updates noch einmal neu gestartet. - Nachprüfung: dieselben Gesundheitsbedingungen wie vorher. Der Lauf scheitert, wenn Nextcloud danach nicht gesund ist.
Die Schritte 3 bis 7 stecken in einem block. Der rescue zeigt den Status, die Pods und Events und die letzten Zeilen von nextcloud.log. Dann trifft er eine vorsichtige Entscheidung: Er verlässt den Wartungsmodus nur, wenn das Upgrade selbst abgeschlossen ist (kein ausstehendes Datenbank-Upgrade). Steht ein Upgrade aus, bleibt der Wartungsmodus an – sonst kämen Nutzer auf eine halb aktualisierte Instanz. Dann hält er an und gibt aus, was als Nächstes zu tun ist.
🛠️ Im Betrieb: Nextcloud im Alltag
Nachsehen
# 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 /dataDie Routine
| Wann | Was | Wie |
|---|---|---|
| Wenn Updates verfügbar sind | Neu gebaute Images, App-Updates, Patch-Releases | ansible-playbook nextcloud-update.yml --limit <host>, zuerst --check |
| Für eine neue Hauptversion | Den Image-Tag in versions.yml ändern – eine Hauptversion nach der anderen | Release Notes lesen, einen Snapshot anlegen, dann ansible-playbook nextcloud-k3s.yml --limit <host> (der Entrypoint führt das Upgrade aus, wenn das neue Image startet) |
| Nach jedem Lauf | Die Verify-Mail lesen | alles grün, oder die Namen der gescheiterten Prüfungen |
| Ab und zu | Die Admin-Übersicht ansehen | Verwaltungseinstellungen → Übersicht listet Warnungen auf, die Nextcloud selbst gefunden hat |
Wenn etwas schiefgeht
- „Office konnte nicht geladen werden“, und ein neuer Versuch klappt: ein kurzer Schluckauf zwischen Browser, Collabora und Nextcloud – meist harmlos. Passiert es oft, braucht Collabora mehr vorgestartete Prozesse oder mehr Speicher (
collabora_num_preallocund das Speicherlimit, pro Host im Inventory). Die große Instanz hält 10 Prozesse in 8 GiB warm, die kleine 6 in 4 GiB. - Nextcloud hängt im Wartungsmodus nach einem gescheiterten Update:
occ statusansehen. Steht ein Upgrade aus,occ upgradeausführen und seine Ausgabe lesen; erst dannocc maintenance:mode --off. Ihn nie abschalten, solange ein Upgrade aussteht. - Nutzer durch die Passwortrichtlinie ausgesperrt:
occ config:app:get password_policy expirationprüfen – es muss 0 sein. Die Rolle legt es fest, aber es ist das Erste, wo man hinschaut. - Die Platte läuft voll: Der Alarm kommt bei 80 Prozent (Teil 6). Der Papierkorb behält gelöschte Dateien 180 Tage und räumt sich selbst auf; der Papierkorb eines einzelnen Nutzers kann trotzdem groß sein. Vorschaubilder lassen sich neu erzeugen und sind keine echte Reserve.
occ trashbin:cleanup --all-usersleert jeden Papierkorb – mit Einverständnis der Nutzer, nicht als Überraschung. - Der ganze Server ist weg: einen neuen mit dem vollen Playbook aufsetzen, dann den Datenbank-Dump, das Anwendungsverzeichnis (mit
config.phpund den installierten Apps) und die Nutzerdaten mit dem Wiederherstellungsskript aus dem externen Backup zurückholen und das Playbook noch einmal laufen lassen. Das Betriebshandbuch im Repository listet jeden Schritt.
🧭 Wie es weitergeht
Mit Teil 4 sind beide Anwendungen abgedeckt. Teil 5 sammelt jede Sicherheitsrolle an einer Stelle – SSH, die Firewall, die CIS-Härtung, SELinux, Audit, AIDE, rkhunter, ClamAV und fail2ban – und erklärt, wogegen jede Schicht schützt. Teil 6 ist das Monitoring: Exporter, Prometheus, Grafana und der Monit-Wachhund, und was jeder Alarm bedeutet.

