Executive Summary #
Der Betrieb von Kubernetes-Clustern, die stateful Workloads (zustandsbehaftete Anwendungen) beherbergen, erfordert rigorose Protokolle für das Lebenszyklusmanagement. Im Gegensatz zu stateless Umgebungen, in denen Nodes als ephemere Ressourcen behandelt werden können, verlangen Cluster, die auf verteiltem Blockspeicher basieren - im Speziellen Longhorn -, eine präzise Orchestrierung während der Shutdown-Prozeduren, um Datenkorruption und Inkonsistenzen auf Volume-Ebene zu verhindern. Zusätzlich führt die Integration von GitOps-Prinzipien durch ArgoCD eine weitere Komplexitätsebene ein: Die Reconciliation-Loops (Abgleichschleifen) müssen kontrolliert ausgesetzt werden, um eine automatisierte, ungewollte Wiederherstellung von Diensten während kritischer Wartungsfenster zu unterbinden. Dieser Artikel liefert eine technische Analyse und einen prozeduralen Leitfaden für das geordnete Herunterfahren und Wiederanfahren eines Kubernetes-Clusters, der auf dem Stack k3s, Longhorn und ArgoCD basiert.
1. Einleitung: Die Notwendigkeit deterministischer Shutdown-Prozeduren #
In der modernen Container-Orchestrierung wird oft angenommen, dass Systeme resilient gegen plötzliche Ausfälle sind. Während Kubernetes hervorragende Mechanismen zur Selbstheilung bietet, gilt dies primär für den Ausfall einzelner Komponenten in einem hochverfügbaren Verbund. Der komplette Shutdown eines Clusters - etwa für Stromwartungen, Rechenzentrumsumzüge oder Hardware-Upgrades - stellt jedoch ein fundamental anderes Szenario dar. Hierbei werden alle Redundanzebenen gleichzeitig entfernt.
1.1 Das Problem der Persistenz in verteilten Systemen #
Bei der Verwendung von Local-Storage oder verteilten Speichersystemen wie Longhorn liegt die Datenhoheit nicht bei einem externen SAN, sondern direkt auf den Compute-Nodes. Ein unkontrolliertes Ausschalten (“Hard Power-off”) eines Nodes, während ein Longhorn-Volume noch als Attached markiert ist, führt unweigerlich zu “Stale Handles” in der Metadaten-Datenbank (etcd). Das System glaubt beim Neustart, das Volume sei noch auf dem alten Node gemountet, und verhindert aus Sicherheitsgründen (RWO - ReadWriteOnce Schutz) das erneute Mounten. Dies führt zu langen Ausfallzeiten, die manuelle Eingriffe in die Custom Resource Definitions (CRDs) erfordern.
1.2 Der GitOps-Konflikt #
ArgoCD fungiert als autonomer Agent, der den Cluster-Zustand permanent gegen ein Git-Repository prüft. In einem Wartungsszenario, bei dem ein Administrator Workloads herunterfährt (skaliert auf 0), interpretiert ArgoCD dies als “Configuration Drift” - eine Abweichung vom Soll-Zustand. Ist “Self-Heal” aktiviert, wird ArgoCD sofort versuchen, die Workloads wieder zu starten. Dies erzeugt eine Race Condition zwischen dem Shutdown-Skript und dem GitOps-Controller.
2. Architektur-Analyse und Abhängigkeitsgraph #
Um ein robustes Protokoll zu entwickeln, muss zunächst das Zusammenspiel der drei Kernkomponenten verstanden werden: Der Orchestrator (k3s), die Storage-Engine (Longhorn) und der Continuous-Delivery-Controller (ArgoCD).
2.1 Die k3s Control Plane und Agent Topologie #
k3s operiert als konsolidierte Kubernetes-Distribution. In einer typischen Deployment-Struktur ist die Service-Architektur in den k3s-server (Control Plane und API Server) und den k3s-agent (Worker Node) unterteilt.
- Prozess-Lebenszyklus: Der Shutdown ist kritisch, da das Kubelet für die geordnete Beendigung von Pods verantwortlich ist. Sendet das Betriebssystem ein SIGKILL an den k3s-Prozess, bevor das Kubelet Zeit hatte, Container-Pre-Stop-Hooks auszuführen, werden Datenbank-Buffer nicht auf die Disk geflusht.
- Systemd Integration: Ein systemctl stop k3s triggert zwar das Beenden der Kubernetes-Dienste, garantiert aber nicht, dass alle von containerd verwalteten Container sauber beendet wurden.
2.2 Longhorn Distributed Storage Mechanik #
Longhorn operiert als Microservices-basiertes verteiltes Blockspeichersystem.
- Risiko des unsauberen Shutdowns: Wird ein Node ausgeschaltet, während ein Longhorn-Volume noch verbunden (attached) ist, hat die Engine keine Zeit, die Verbindung sauber zu schließen. In der etcd-Datenbank verbleibt das Volume im Status Attached oder Faulted.
- RWO Restriktion: Die meisten Longhorn-Volumes sind ReadWriteOnce (RWO). Wenn der Cluster nicht sauber heruntergefahren wird, bleibt der Lock auf dem Volume bestehen.
2.3 ArgoCD Reconciliation Loops #
ArgoCD erzwingt den im Git definierten Zustand.
- Konfliktpotenzial: Wenn ein Administrator manuell Deployments herunterskaliert, um Longhorn-Volumes freizugeben, erkennt ArgoCD dies als “Out of Sync”.
- Lösung: Der erste Schritt jedes Wartungsprotokolls muss die globale Suspension des Reconciliation-Loops sein.
3. Strategische Planung des Shutdown-Protokolls #
Das Shutdown-Prozedere wird durch eine strikte Abhängigkeitskette definiert.
Phase 1: Globale GitOps-Suspension #
Wir nutzen das Skalieren des argocd-application-controller StatefulSets auf Null. Dies “friert” den Cluster-Zustand effektiv ein und garantiert, dass keine Hintergrundprozesse versuchen, Ressourcen zu modifizieren.
Phase 2: Applikations-Dekommissionierung (Workload Draining) #
Ist ArgoCD pausiert, müssen zustandsbehaftete Workloads auf Null skaliert werden. Dies fungiert als Trigger für das Container Storage Interface (CSI), um ControllerUnpublishVolume-Aufrufe an Longhorn zu senden.
- Identifikation: Wir stoppen alle Deployments und StatefulSets in User-Namespaces.
- Verifikation: Ein Pod im Status Terminating kann immer noch einen Lock auf ein Longhorn-Volume halten.
Phase 3: Verifikation der Speicher-Detachment (Abtrennung) #
Dies ist der wichtigste Sicherheitscheck. Die Longhorn API muss abgefragt werden. Wenn irgendein Volume status.state: attached meldet, muss der Node-Shutdown abgebrochen werden.
- Wichtig: Longhorn-Systemkomponenten (Manager, Driver) müssen weiterlaufen, bis alle User-Volumes abgetrennt sind.
Phase 4: Infrastruktur-Shutdown #
- Stop Agents: Verhindert neues Scheduling.
- Stop Server: Terminiert die Control Plane.
- OS Shutdown: shutdown -h now.
4. Automatisierung via Ansible #
Die manuelle Ausführung ist fehleranfällig. Nachfolgend die Ansible-Implementierung.
Voraussetzungen #
- Ansible Core + kubernetes.core Collection.
- Python-Libraries: kubernetes, jsonpatch, PyYAML.
- Gültige kubeconfig.
Das Shutdown-Playbook (shutdown_cluster.yml) #
1---
2- name: Graceful Cluster Shutdown Protocol (Geordnetes Herunterfahren)
3 hosts: localhost
4 connection: local
5 gather_facts: no
6 vars:
7 # Pfad zur Kubeconfig - Anpassen falls notwendig
8 kubeconfig_path: "~/.kube/config"
9 longhorn_ns: "longhorn-system"
10 argocd_ns: "argocd"
11 detachment_timeout: 300
12
13 tasks:
14 # ---------------------------------------------------------
15 # Phase 1: ArgoCD Reconciliation Suspendieren
16 # ---------------------------------------------------------
17 - name: Prüfe Status des ArgoCD Application Controllers
18 kubernetes.core.k8s_info:
19 kubeconfig: "{{ kubeconfig_path }}"
20 kind: StatefulSet
21 namespace: "{{ argocd_ns }}"
22 name: argocd-application-controller
23 register: argocd_controller_state
24
25 - name: Skaliere ArgoCD Application Controller auf 0 (Pause Reconciliation)
26 kubernetes.core.k8s_scale:
27 kubeconfig: "{{ kubeconfig_path }}"
28 api_version: apps/v1
29 kind: StatefulSet
30 name: argocd-application-controller
31 namespace: "{{ argocd_ns }}"
32 replicas: 0
33 wait: yes
34 when: argocd_controller_state.resources | length > 0
35 tags: [argocd]
36
37 # ---------------------------------------------------------
38 # Phase 2: Skalieren der Stateful Workloads
39 # ---------------------------------------------------------
40 - name: Hole alle Deployments im Cluster
41 kubernetes.core.k8s_info:
42 kubeconfig: "{{ kubeconfig_path }}"
43 kind: Deployment
44 register: all_deployments
45
46 - name: Hole alle StatefulSets im Cluster
47 kubernetes.core.k8s_info:
48 kubeconfig: "{{ kubeconfig_path }}"
49 kind: StatefulSet
50 register: all_statefulsets
51
52 - name: Skaliere alle User-Deployments auf 0 herunter
53 kubernetes.core.k8s_scale:
54 kubeconfig: "{{ kubeconfig_path }}"
55 api_version: apps/v1
56 kind: Deployment
57 name: "{{ item.metadata.name }}"
58 namespace: "{{ item.metadata.namespace }}"
59 replicas: 0
60 wait: yes
61 loop: "{{ all_deployments.resources }}"
62 loop_control:
63 label: "{{ item.metadata.namespace }}/{{ item.metadata.name }}"
64 when:
65 - item.metadata.namespace not in ['kube-system', 'longhorn-system', 'argocd', 'monitoring']
66 - item.spec.replicas > 0
67 ignore_errors: yes
68 tags: [workloads]
69
70 - name: Skaliere alle User-StatefulSets auf 0 herunter
71 kubernetes.core.k8s_scale:
72 kubeconfig: "{{ kubeconfig_path }}"
73 api_version: apps/v1
74 kind: StatefulSet
75 name: "{{ item.metadata.name }}"
76 namespace: "{{ item.metadata.namespace }}"
77 replicas: 0
78 wait: yes
79 loop: "{{ all_statefulsets.resources }}"
80 loop_control:
81 label: "{{ item.metadata.namespace }}/{{ item.metadata.name }}"
82 when:
83 - item.metadata.namespace not in ['kube-system', 'longhorn-system', 'argocd', 'monitoring']
84 - item.spec.replicas > 0
85 tags: [workloads]
86
87 # ---------------------------------------------------------
88 # Phase 3: Verifikation des Longhorn Volume Detachments
89 # ---------------------------------------------------------
90 - name: Warte darauf, dass alle Longhorn Volumes den Status 'detached' haben
91 kubernetes.core.k8s_info:
92 kubeconfig: "{{ kubeconfig_path }}"
93 api_version: longhorn.io/v1beta2
94 kind: Volume
95 namespace: "{{ longhorn_ns }}"
96 register: longhorn_volumes
97 until: "longhorn_volumes.resources | json_query('[?status.state!=\\'detached\\']') | length == 0"
98 retries: "{{ (detachment_timeout / 10) | int }}"
99 delay: 10
100 msg: "Warte auf Detachment. Prüfen Sie auf hängende Pods, falls dies fehlschlägt."
101 tags: [storage]
102
103# ---------------------------------------------------------
104# Phase 4: Stop k3s Services auf den physischen Nodes
105# ---------------------------------------------------------
106- name: Stop k3s Services und Shutdown Nodes
107 hosts: all
108 become: yes
109 gather_facts: no
110 tags: [infrastructure]
111 tasks:
112 - name: Stoppe k3s-agent (Worker Nodes)
113 ansible.builtin.systemd:
114 name: k3s-agent
115 state: stopped
116 when: "'k3s_agent' in group_names"
117 ignore_errors: yes
118
119 - name: Stoppe k3s (Server/Master Nodes)
120 ansible.builtin.systemd:
121 name: k3s
122 state: stopped
123 when: "'k3s_server' in group_names"
124
125 - name: Shutdown Operating System
126 community.general.shutdown:
127 delay: 1
128 msg: "Automatischer Shutdown durch Ansible Playbook"
5. Das Recovery-Protokoll (Wiederanfahren) #
Das Starten des Clusters erfordert die Umkehrung der Reihenfolge. Das Speichersystem muss gesund sein, bevor die Anwendungen hochskaliert werden.
Das Startup-Playbook (startup_cluster.yml) #
1---
2- name: Graceful Cluster Startup Protocol (Startvorgang)
3 hosts: all
4 become: yes
5 tags: [infrastructure]
6 tasks:
7 # ---------------------------------------------------------
8 # Phase 1: Nodes Booten und k3s Starten
9 # ---------------------------------------------------------
10 # Voraussetzung: Nodes sind via Wake-on-LAN oder physisch gestartet
11
12 - name: Starte k3s (Server/Master)
13 ansible.builtin.systemd:
14 name: k3s
15 state: started
16 enabled: yes
17 when: "'k3s_server' in group_names"
18
19 - name: Starte k3s-agent (Worker)
20 ansible.builtin.systemd:
21 name: k3s-agent
22 state: started
23 enabled: yes
24 when: "'k3s_agent' in group_names"
25
26- name: Orchestrate Workload Recovery
27 hosts: localhost
28 connection: local
29 gather_facts: no
30 vars:
31 kubeconfig_path: "~/.kube/config"
32 longhorn_ns: "longhorn-system"
33 argocd_ns: "argocd"
34
35 tasks:
36 # ---------------------------------------------------------
37 # Phase 2: Verifikation der Storage-Gesundheit
38 # ---------------------------------------------------------
39 - name: Warte bis Longhorn Nodes 'Ready' sind
40 kubernetes.core.k8s_info:
41 kubeconfig: "{{ kubeconfig_path }}"
42 api_version: longhorn.io/v1beta2
43 kind: Node
44 namespace: "{{ longhorn_ns }}"
45 register: lh_nodes
46 until: lh_nodes.resources | length > 0
47 retries: 30
48 delay: 10
49 tags: [storage]
50
51 - name: Warte auf Longhorn System Pods
52 kubernetes.core.k8s_info:
53 kubeconfig: "{{ kubeconfig_path }}"
54 kind: Pod
55 namespace: "{{ longhorn_ns }}"
56 field_selectors:
57 - status.phase=Running
58 register: lh_pods
59 until: lh_pods.resources | length >= 3
60 retries: 30
61 delay: 10
62
63 # ---------------------------------------------------------
64 # Phase 3: ArgoCD Reaktivierung
65 # ---------------------------------------------------------
66 # Durch das Hochskalieren erkennt ArgoCD den Drift (0 vs 1 im Git)
67 # und stellt die Applikationen automatisch wieder her.
68
69 - name: Skaliere ArgoCD Application Controller auf 1
70 kubernetes.core.k8s_scale:
71 kubeconfig: "{{ kubeconfig_path }}"
72 api_version: apps/v1
73 kind: StatefulSet
74 name: argocd-application-controller
75 namespace: "{{ argocd_ns }}"
76 replicas: 1
77 wait: yes
78 tags: [argocd]
79
80 - name: Warte auf ArgoCD Reconciliation
81 ansible.builtin.debug:
82 msg: "ArgoCD Controller neugestartet. Auto-Sync wird nun initiiert."
6. Deep Dive: Fehleranalyse und Edge Cases #
Blockierende Finalizers auf PVCs #
Ein häufiges Problem beim Shutdown ist, dass Ressourcen im Status Terminating hängen bleiben. Ein Pod im Status Terminating kann immer noch einen Lock auf ein Longhorn-Volume halten. Das Ansible Playbook verlässt sich darauf, dass k8s_scale den Controller zwingt, die Pods zu entfernen. Reagiert das Kubelet auf einem Node nicht, bleibt das Pod-Objekt bestehen.
Die “ArgoCD Fighting Back” Race Condition #
Wird der argocd-application-controller nicht auf 0 skaliert bevor die Anwendungen heruntergefahren werden, entsteht eine Race Condition:
- Ansible skaliert deployment/my-app auf 0.
- ArgoCD erkennt Drift (Live: 0, Git: 1) und patched zurück auf 1.
- Longhorn versucht, das Volume neu zu attachen, während Ansible den Node herunterfährt.
- Resultat: Volume-Korruption.
7. Fazit #
Das sichere Herunterfahren eines k3s-Clusters mit persistentem Speicher ist fundamental eine Übung im Abhängigkeitsmanagement. Die Hierarchie lautet:
- GitOps Layer (ArgoCD): Muss zuerst verstummen.
- Application Layer: Muss terminiert werden (Volume Release).
- Storage Layer (Longhorn): Muss Detachment bestätigen.
- Infrastruktur Layer (k3s/OS): Darf erst am Ende ausgeschaltet werden.
Durch die Kapselung dieser Logik in Ansible Playbooks wird das Risiko von “Split-Brain”-Szenarien in der Storage-Engine signifikant reduziert.
Per E-Mail antworten