k3s: Portainer CE mit übernommener Datenbank von appsrv08 (Manifest + Playbook mit Rollback BE→CE)
ci / test (push) Failing after 24s

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Robert Rapp
2026-10-15 18:45:37 +02:00
co-authored by Claude Fable 5.1
parent 26e1079f3d
commit 4d31dda0e5
2 changed files with 198 additions and 0 deletions
+95
View File
@@ -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://<Knoten-IP>: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}
@@ -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 <<EOF | kubectl apply -f - >/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"