Zum Hauptinhalt springen
  1. Beiträge/
  2. Kleines Homelab/
  3. k3s-prod: Kubernetes Cluster Konfiguration und Anwendungsbereitstellung/

Operational Lifecycle Management: Orchestrierter Shutdown für Kubernetes

··1830 Wörter· ·
Inhaltsverzeichnis

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
#

  1. Stop Agents: Verhindert neues Scheduling.
  2. Stop Server: Terminiert die Control Plane.
  3. 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:

  1. Ansible skaliert deployment/my-app auf 0.
  2. ArgoCD erkennt Drift (Live: 0, Git: 1) und patched zurück auf 1.
  3. Longhorn versucht, das Volume neu zu attachen, während Ansible den Node herunterfährt.
  4. Resultat: Volume-Korruption.

7. Fazit
#

Das sichere Herunterfahren eines k3s-Clusters mit persistentem Speicher ist fundamental eine Übung im Abhängigkeitsmanagement. Die Hierarchie lautet:

  1. GitOps Layer (ArgoCD): Muss zuerst verstummen.
  2. Application Layer: Muss terminiert werden (Volume Release).
  3. Storage Layer (Longhorn): Muss Detachment bestätigen.
  4. 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
Fabrice Kirchner
Autor
Fabrice Kirchner
stolzer Vater, Nerd, Admin