01 Wo alles steht
Der Ist-Zustand nach dem Umzug – damit die restlichen Abschnitte konkret sind.
shared-jenkins im shared-infra-Compose. UI http://localhost:8080, Login admin / admin.426664299 (~4–6 GB, ~0,10 €/Mon) bleibt. Zurück mit .\scripts\jenkins.ps1 up.okd-sno, Namespace library. Seit dem Tailscale-Umbau ist die OKD-API (6443) nur noch übers Tailnet erreichbar – der shared-jenkins-Container erbt den hosts-Eintrag des Laptops (100.67.147.8 api.sno.<okd-ip>.nip.io) und kommt darüber an den Cluster.
Die IP-Beispiele unten (
62.238.124.7) sind der Stand zum Zeitpunkt dieses Laufs – deine steht in .\scripts\sno.ps1 status und wechselt bei jedem sno.ps1 install.#20 grün (19,5 min, kalt) · #21 rot (Registry-Blip) · #22 grün (2,5 min, warm).#1–17, Credentials github-library-deploykey / KUBECONFIG_PROD / okd-registry.02 Warum lokal
Ein zweiter Server für ein Projekt mit gelegentlichen Deploys ist Overhead.
Auf Hetzner lief ein cpx42 (~0,10 €/h) plus die Firewall-Choreografie, die der
Pipeline Zugriff auf die OKD-API verschafft (jenkins.ps1 trägt die Server-IP bei
jedem up in okd-sno-fw:6443 ein und bei down wieder aus). Für den
aktuellen Bedarf reicht ein Jenkins in der ohnehin laufenden podman-machine.
Der Hetzner-Weg bleibt erhalten: jenkins.ps1 up baut den Server aus dem Snapshot in
~95 s wieder auf. Der Snapshot ist die Rückfahrkarte, jenkins.ps1 destroy verbrennt sie.
Der komplette „25 Fallen“-Apparat aus der
OKD-Deploy-Anleitung steckt als
init.groovy.d/-Skripte im jenkins_home und läuft bei jedem Boot
idempotent: 05-git-hostkey.groovy (GIT_SSH_COMMAND), 10-okd-creds.groovy
(legt KUBECONFIG_PROD + okd-registry aus okd.kubeconfig + okd.token
neu an), 21-job-scm.groovy (biegt den Job auf */okd-deploy + Deploy-Key
um, hängt DEPLOY_ONLY an).
03 Wie der Stand rüberkam
„Weg B“: den laufenden Stand aus dem Server ziehen statt neu aufzusetzen. Nachbaubar, falls es nochmal nötig wird.
- Container stoppen für eine konsistente Kopie.
ssh root@<jenkins-ip> "podman stop -t 30 jenkins"
/opt/jenkins-homeals Tar ziehen – 750 MB roh, 333 MB gepackt.
ssh root@<jenkins-ip> "tar -C /opt/jenkins-home --numeric-owner -czf - ." \ > jenkins-home.tar.gz
jenkins.ps1 down– Snapshot426664299, Server gelöscht, diejenkins-auto-Regel inokd-sno-fwentfernt.- Tar ins Volume
shared-infra_jenkins-dataentpacken über einen Helfer-Container – altes lokalesjenkins_homevorher weg.
find /dest -mindepth 1 -delete tar -C /dest -xzf /src/jenkins-home.tar.gz chown -R 1000:1000 /dest
- Starten.
cd shared-infra podman compose up -d jenkins
Ergebnis: 4 Jobs, Build-Historie library-platform-pipeline #1–17, alle drei
Credentials. credentials.xml + secret.key + secrets/ kommen im
Tar mit, deshalb bleiben die verschlüsselten Credentials benutzbar.
04 Der podman-compose-Trick
Der einzige echte Umbau: den Podman-Socket der podman-machine in den Jenkins-Container bekommen, damit die podman build-Stufen laufen.
podman compose nutzt hier podman-compose 1.6.0 (Python, via scoop). Das schreibt
unter Windows in assert_volume() jeden absoluten Bind-Mount-Pfad über
os.path.abspath(os.path.join(basedir, src)) auf C:\… um – auch die
Langform mit type: bind – und legt dort per os.makedirs leere Ordner an.
Quelle /run/user/1000/podman/podman.sock → podman-compose → im Container
/mnt/c/run/user/1000/podman/podman.sock. Existiert nicht in der VM,
Socket tot, leerer Ordner auf C:\.
driver_opts: { o: bind, device: /run/user/1000/podman } geht
unverändert an podman volume create und wird von podman
in der VM aufgelöst. Echter Socket im Container.
Gebunden wird das Verzeichnis /run/user/1000/podman – die
Socket-Datei selbst lässt sich nicht auf das _data-Dir des Volumes mounten.
Gemountet nach /run/podman-host. Ein zweiter Mount desselben Volumes auf
/var/run/docker.sock verdrängt den kaputten Basis-Mount aus docker-compose.yml.
volumes:
podman-sock:
driver: local
driver_opts:
type: none
o: bind
device: /run/user/1000/podman
services:
jenkins:
ports:
- "8080:8080" # UI direkt - Traefik bindet rootless kein :80
- "50000:50000"
restart: unless-stopped
environment:
CONTAINER_HOST: unix:///run/podman-host/podman.sock
DOCKER_HOST: unix:///run/podman-host/podman.sock
volumes:
- podman-sock:/run/podman-host
- podman-sock:/var/run/docker.sock # verdraengt den Basis-Mountpodman build aus dem Jenkins-Container gegen die Host-Engine läuft, und die
Verdrahtung übersteht podman machine stop / start.
05 Dateien & Maschine
Was neu ist – und was in der podman-machine gesetzt werden musste.
Neu in shared-infra/ (dort kein Git-Repo)
CONTAINER_ENGINE_SOCKET bleibt bewusst ungesetzt – der Socket kommt über das Volume oben.podman compose automatisch mitgeladen: Host-Port 8080, restart: unless-stopped, CONTAINER_HOST/DOCKER_HOST, das podman-sock-Volume.Einmalig in der podman-machine
Per podman machine ssh. Verschwindet bei podman machine rm und muss dann neu gesetzt werden.
# shared-jenkins nach `podman machine start` selbst hochfahren sudo systemctl enable podman-restart.service sudo chown -R user:user ~/.config/systemd systemctl --user enable podman-restart.service # `podman build -v` loest die -v-Quelle in der VM auf sudo mkdir -p /opt/m2cache && sudo chmod 777 /opt/m2cache
Und einmalig im Container, weil workspace/ mit kaputtem @script/.git im Tar lag:
podman exec shared-jenkins rm -rf /var/jenkins_home/workspace/*
podman-machine ist größer als sie aussieht. podman machine list zeigt
„2 GiB“ – das ist nominell, WSL2 vergibt RAM dynamisch (real ~16 GB / 8 CPU).
podman machine set --cpus/--memory geht bei WSL-Maschinen nicht.
06 Täglicher Betrieb
Start, Stop, und der Weg zurück auf Hetzner.
# nach `podman machine start` kommt Jenkins per podman-restart.service selbst; # explizit: cd shared-infra && podman compose up -d jenkins # stoppen - Daten bleiben im Volume jenkins-data podman compose stop jenkins
- UI:
http://localhost:8080–admin/admin. CASC setzt das Passwort bei jedem Boot neu ausJENKINS_ADMIN_PASSWORD, egal was im wiederhergestelltenusers/steht. - Zurück auf Hetzner:
.\scripts\jenkins.ps1 up– Server aus Snapshot, gleiche Firewall-Automatik. - Snapshot endgültig weg:
.\scripts\jenkins.ps1 destroy.
07 Deploy auslösen
DEPLOY_ONLY=true überspringt Test-, Lint- und Frontend-Stufen und geht direkt Build → Push in die OKD-Registry → helm upgrade.
Der Crumb muss über einen Cookie-Jar mit – sonst HTTP 403 (Falle 11 der OKD-Deploy-Anleitung).
J='http://admin:admin@localhost:8080' CRUMB=$(curl -s -c cj "$J/crumbIssuer/api/json" \ | python -c "import sys,json;print(json.load(sys.stdin)['crumb'])") curl -s -b cj -H "Jenkins-Crumb:$CRUMB" -X POST \ "$J/job/library-platform-pipeline/buildWithParameters" \ --data-urlencode "DEPLOY_ONLY=true"
Oder in der UI: library-platform-pipeline → „Build mit Parametern“. OKD muss
laufen (sno.ps1 up).
Die Deploy-Stufen reden mit der OKD-API auf 6443 – seit dem
Tailscale-Umbau tailnet-only. Der Laptop braucht den
hosts-Eintrag <tailscale-ip> api.sno.<okd-ip>.nip.io und eine
aktive Tailscale-Verbindung. Der shared-jenkins-Container erbt beides über die
podman-machine (geprüft: kubectl get ns library aus dem Container
löst den Namen auf die Tailnet-IP auf und antwortet). Die Image-Pushes gehen dagegen
an die Registry-Route auf 80/443 – die bleibt öffentlich.
08 Stolpersteine (#18–#22)
Was in den ersten lokalen Läufen hochkam – und der Fix.
| Build | Symptom | Ursache | Fix |
|---|---|---|---|
| #18 | Checkout: fatal: not in a git directory |
workspace/ aus dem Tar mit kaputtem @script/.git |
rm -rf /var/jenkins_home/workspace/* |
| #19 | podman build: validating volumes: faccessat /opt/m2cache: no such file or directory |
-v-Quelle wird gegen die VM aufgelöst, dort fehlte sie |
mkdir -p /opt/m2cache && chmod 777 in der podman-machine |
| #21 | podman login → dial tcp 62.238.124.7:80: connection refused |
openshift-apiserver auf dem Single-Node kurz neu → Route-API weg → Ingress-Router NotReady → *.apps.sno… komplett offline |
selbstheilend in 1–2 min, dann neu triggern |
| #22 | einzelner helm upgrade → cluster unreachable: i/o timeout |
kurzer 6443-Blip, Pipeline fiel für den Service auf helm template-Dry-Run zurück |
nicht-fatal – Service lag ohnehin schon auf dem Tag |
| – | Traefik / jenkins.localhost reagiert nicht |
rootless podman bindet Port 80 nicht | Override gibt Jenkins direkt :8080, Traefik bleibt außen vor |
#21 und der Blip in #22 sind SNO-Fragilität unter Last, nicht das lokale
Setup: ein winziger Node trägt OKD-Plattform + Monitoring + 10 Services +
Kafka/Keycloak/Postgres, und helm --wait plus Image-Pushes treiben etcd/apiserver
an die Grenze. Sauberer Fix wäre ein retry-Block auf der Push-/Deploy-Stage im
Jenkinsfile des Ziel-Repos.
09 Verifikation
Zwei komplette Läufe vom lokalen Jenkins nach OKD.
helm upgrade --install … --wait --timeout 3m, alle STATUS: deployed.fine-service → Dry-Run-Fallback, Rest deployte.library.apps.sno.62.238.124.7.nip.io/actuator/health → {"status":"UP"} · Frontend HTTP 200 · 9/9 Library-Pods ready · alle Deployments auf Tag 597452ba.+ Was noch aussteht
- Jenkinsfile:
retry-Block auf Push- und Deploy-Stage, um die SNO-Blips abzufangen – Änderung im Ziel-RepoLAB-WIP-MAVEN-Bibliothek-Enterprise. okd.kubeconfig/okd.tokenimjenkins_homesind auf die OKD-IP zum Snapshot-Zeitpunkt gebacken. Nach einemsno.ps1 install(neue IP) erneuern –10-okd-creds.groovylegt die Credentials daraus bei jedem Boot neu an.- Traefik lokal ungelöst (rootless kein Port 80). Der direkte
:8080reicht für den Jenkins-Betrieb.
+ Bezug zu den Skripten
up / down / destroy für den Hetzner-Server. down zieht den Snapshot, den dieses Runbook auspackt..override.yml daneben ist lokal, unversioniert.Dockerfile + casc.yaml + plugins.txt – das Jenkins-Image (Maven 3.9.9, Node 20, Helm, kubectl, Chromium). Baut lokal wie auf dem Server.