From 4d31dda0e5fad54dbad7a0902168c72c40e327cb Mon Sep 17 00:00:00 2001 From: Robert Rapp Date: Thu, 15 Oct 2026 18:45:37 +0200 Subject: [PATCH] =?UTF-8?q?k3s:=20Portainer=20CE=20mit=20=C3=BCbernommener?= =?UTF-8?q?=20Datenbank=20von=20appsrv08=20(Manifest=20+=20Playbook=20mit?= =?UTF-8?q?=20Rollback=20BE=E2=86=92CE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- deploy/k8s/portainer/portainer.yaml | 95 ++++++++++++++++ .../ansible/playbooks/portainer-k3s.yml | 103 ++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 deploy/k8s/portainer/portainer.yaml create mode 100644 platform/proxmox/ansible/playbooks/portainer-k3s.yml diff --git a/deploy/k8s/portainer/portainer.yaml b/deploy/k8s/portainer/portainer.yaml new file mode 100644 index 0000000..f6a7552 --- /dev/null +++ b/deploy/k8s/portainer/portainer.yaml @@ -0,0 +1,95 @@ +# Portainer CE im Cluster, Umzug von appsrv08 (Portainer EE 2.45.1) mit übernommener Datenbank (Benutzer, +# API-Schlüssel, Umgebungen, Stacks). Läuft auf der Dienst-VM k3s (tank, HA), Daten auf local-path dieser VM. +# Erst-Befüllung und Rollback BE→CE macht playbooks/portainer-k3s.yml aus /root/.config/knaster/portainer_data.tgz. +# Erreichbar: https://:30779 (NodePort), von außen über den nginx auf appsrv08 (portainer.mygodel.de). +# Dieselbe Version wie auf appsrv08 (2.45.1): die Datenbank wird beim Start nicht migriert. +apiVersion: v1 +kind: Namespace +metadata: + name: portainer-ce + labels: + knaster.app: portainer +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: portainer-data + namespace: portainer-ce +spec: + accessModes: [ReadWriteOnce] + storageClassName: local-path + resources: + requests: + storage: 2Gi +--- +# Portainer verwaltet den eigenen Cluster über ein ServiceAccount (Umgebung „local“ wird beim ersten Start angelegt). +apiVersion: v1 +kind: ServiceAccount +metadata: + name: portainer-sa-clusteradmin + namespace: portainer-ce +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: portainer-ce-crb +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: portainer-sa-clusteradmin + namespace: portainer-ce +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: portainer + namespace: portainer-ce + labels: {app: portainer-ce} +spec: + replicas: 1 + strategy: {type: Recreate} + selector: + matchLabels: {app: portainer-ce} + template: + metadata: + labels: {app: portainer-ce} + spec: + serviceAccountName: portainer-sa-clusteradmin + nodeSelector: + knaster.role: control # Dienst-VM k3s: tank, HA, Replikation + containers: + - name: portainer + image: portainer/portainer-ce:2.45.1 + ports: + - {name: https, containerPort: 9443} + - {name: edge, containerPort: 8000} + resources: + requests: {cpu: 50m, memory: 128Mi} + limits: {memory: 512Mi} + volumeMounts: + - {name: data, mountPath: /data} + readinessProbe: + httpGet: {path: /api/system/status, port: https, scheme: HTTPS} + periodSeconds: 10 + livenessProbe: + httpGet: {path: /api/system/status, port: https, scheme: HTTPS} + periodSeconds: 30 + failureThreshold: 3 + volumes: + - name: data + persistentVolumeClaim: {claimName: portainer-data} +--- +apiVersion: v1 +kind: Service +metadata: + name: portainer + namespace: portainer-ce + labels: {app: portainer-ce} +spec: + type: NodePort + selector: {app: portainer-ce} + ports: + - {name: https, port: 9443, targetPort: https, nodePort: 30779} diff --git a/platform/proxmox/ansible/playbooks/portainer-k3s.yml b/platform/proxmox/ansible/playbooks/portainer-k3s.yml new file mode 100644 index 0000000..bf0f94d --- /dev/null +++ b/platform/proxmox/ansible/playbooks/portainer-k3s.yml @@ -0,0 +1,103 @@ +# Portainer CE in k3s mit der Datenbank von appsrv08 (Portainer EE 2.45.1). Läuft auf ops: +# ansible-playbook playbooks/portainer-k3s.yml +# +# Voraussetzung: Sicherung des Docker-Volumes portainer_data von appsrv08 als /root/.config/knaster/portainer_data.tgz +# (Inhalt: data/portainer.db, data/portainer.key, …; erzeugt mit +# docker run --rm -v portainer_data:/data:ro alpine tar -C / -czf - data > portainer_data.tgz ). +# Beim ersten Lauf wird das Volume befüllt und die Datenbank mit dem offiziellen Werkzeug der EE +# (`--rollback-to-ce`) auf Community Edition zurückgesetzt; danach bleibt der Datenstand im Cluster. +# Benutzer, API-Schlüssel und Umgebungen bleiben erhalten (getestet 15.10.2026). Die Umgebung 4 (Docker-Socket +# von appsrv08) funktioniert im Cluster nicht mehr; appsrv08 wird als Agent-Umgebung neu angebunden. +- name: Portainer CE nach k3s + hosts: localhost + connection: local + gather_facts: false + vars: + manifest: "{{ playbook_dir }}/../../../../deploy/k8s/portainer/portainer.yaml" + backup: /root/.config/knaster/portainer_data.tgz + ns: portainer-ce + version: 2.45.1 + tasks: + - name: Namespace, PVC, RBAC (noch ohne Deployment, damit das Volume zuerst befüllt wird) + ansible.builtin.shell: > + set -o pipefail; + python3 -c "import sys,yaml; docs=[d for d in yaml.safe_load_all(open('{{ manifest }}')) if d and d['kind']!='Deployment' and d['kind']!='Service']; yaml.safe_dump_all(docs, sys.stdout)" + | kubectl apply -f - + args: {executable: /bin/bash} + register: base + changed_when: "'created' in base.stdout" + + - name: Ist das Volume schon befüllt? + ansible.builtin.command: kubectl -n {{ ns }} get configmap portainer-seeded + register: seeded + failed_when: false + changed_when: false + + - name: Sicherung vorhanden? + ansible.builtin.stat: + path: "{{ backup }}" + register: bk + when: seeded.rc != 0 + failed_when: not bk.stat.exists + + - name: Volume aus der Sicherung befüllen (Hilfs-Pod auf dem Knoten k3s) + ansible.builtin.shell: | + set -e + kubectl -n {{ ns }} delete pod seed --ignore-not-found >/dev/null + cat </dev/null + apiVersion: v1 + kind: Pod + metadata: {name: seed, namespace: {{ ns }}} + spec: + nodeSelector: {knaster.role: control} + restartPolicy: Never + containers: + - name: seed + image: alpine:3.20 + command: ["sleep", "600"] + volumeMounts: [{name: data, mountPath: /data}] + volumes: [{name: data, persistentVolumeClaim: {claimName: portainer-data}}] + EOF + kubectl -n {{ ns }} wait --for=condition=Ready pod/seed --timeout=180s >/dev/null + kubectl -n {{ ns }} cp {{ backup }} seed:/tmp/pd.tgz + kubectl -n {{ ns }} exec seed -- sh -c "cd /tmp && tar xzf pd.tgz && cp -a data/. /data/ && test -s /data/portainer.db" + kubectl -n {{ ns }} delete pod seed --wait=true >/dev/null + when: seeded.rc != 0 + + - name: Datenbank auf Community Edition zurücksetzen (offizielles Werkzeug der EE, fragt per stdin) + ansible.builtin.shell: | + set -e + kubectl -n {{ ns }} delete pod rollback --ignore-not-found >/dev/null + echo y | kubectl -n {{ ns }} run rollback -i --restart=Never --image=portainer/portainer-ee:{{ version }} \ + --overrides='{"spec":{"nodeSelector":{"knaster.role":"control"},"containers":[{"name":"rollback","image":"portainer/portainer-ee:{{ version }}","args":["--rollback-to-ce"],"stdin":true,"volumeMounts":[{"name":"data","mountPath":"/data"}]}],"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"portainer-data"}}]}}' >/dev/null 2>&1 || true + sleep 5 + kubectl -n {{ ns }} logs rollback | grep -q "rolled back to CE Edition" + kubectl -n {{ ns }} delete pod rollback --wait=false >/dev/null + kubectl -n {{ ns }} create configmap portainer-seeded --from-literal=source={{ backup | basename }} --from-literal=date="$(date -Is)" + when: seeded.rc != 0 + + - name: Deployment und Service + ansible.builtin.command: kubectl apply -f {{ manifest }} + register: dep + changed_when: "'configured' in dep.stdout or 'created' in dep.stdout" + + - name: Warten, bis bereit + ansible.builtin.command: kubectl -n {{ ns }} rollout status deployment/portainer --timeout=300s + changed_when: false + + - name: Version und Edition über NodePort (Knoten k3s) + ansible.builtin.uri: + url: "https://{{ hostvars['k3s'].ansible_host }}:30779/api/system/version" + validate_certs: false + return_content: true + register: ver + until: ver.status == 200 + retries: 6 + delay: 5 + + - name: Ergebnis + ansible.builtin.assert: + that: + - ver.json.ServerEdition == 'CE' + - ver.json.ServerVersion == version + success_msg: "Portainer {{ ver.json.ServerEdition }} {{ ver.json.ServerVersion }} unter https://{{ hostvars['k3s'].ansible_host }}:30779"