Skip to content

Quelle

Diese Seite ist abgeleitet. Bitte Änderungen in der kanonischen SSOT-Fassung vornehmen: source-of-truth/handbooks/operator/tcloud-lifecycle.md. Diagramm-Quellen (PlantUML) liegen unter source-of-truth/diagrams/operator/.

Operator-Handbuch — T-Cloud Lifecycle

Was dieses Buch ist. Eine vollständige, von Hand abarbeitbare Anleitung, um die Officina-del-Caffè-Appliance auf der T-Cloud (Open Telekom Cloud, OTC) komplett hochzufahren und wieder vollständig herunterzufahren — von der nackten Infrastruktur bis zur erreichbaren Live-URL und zurück auf null.

Qualitätsziel. Eine menschliche Operator:in ist nach diesem Buch vollständig handlungsfähig — ohne maschinellen AI-Agenten. Jeder Schritt nennt das Arbeitsverzeichnis, die nötigen Umgebungsvariablen, das genaue Kommando, einen Beispiel-Output und — wo relevant — was in der OTC-Console zu tun ist.


Inhalt

  1. Wie man dieses Buch liest
  2. Architektur auf einen Blick
  3. Werkzeuge & Voraussetzungen
  4. Credentials laden (age-Bundle + OS_-Mapping)
  5. Kontext-Konventionen (cwd, ENV, KUBECONFIG)
  6. HOCHFAHREN — Schritt für Schritt
  7. HERUNTERFAHREN — geordneter Abriss
  8. Statussequenzen
  9. Bekannte Probleme & Lösungen
  10. OTC-Console-Cheatsheet
  11. Health- & Smoke-Checks
  12. Was bewusst bestehen bleibt

1. Wie man dieses Buch liest

  • Jeder Befehlsblock ist mit einem Kontext-Header versehen. Vor jedem Block steht, in welchem Verzeichnis Sie stehen müssen (cwd:) und welche ENV-Variablen gesetzt sein müssen (env:). Wenn Sie unsicher sind, prüfen Sie zuerst Kapitel 5.
  • Platzhalter stehen in spitzen Klammern, z. B. <EIP>, <rev>. Ersetzen Sie sie durch echte Werte aus den jeweils vorherigen Outputs.
  • OTC-Console meint das Web-Portal unter https://console.otc.t-systems.com (Browser, Login mit Ihrem OTC-Account). Schritte, die zwingend dort passieren, sind mit dem Symbol 🖱️ Console markiert.
  • Gefahrenstellen sind mit ⚠️ markiert. Lesen Sie diese, bevor Sie das Kommando absenden — mehrere davon haben uns in früheren Sessions Stunden gekostet.
  • Reihenfolge ist nicht optional. Beim Teardown insbesondere ist die Lösch-Reihenfolge der OTC-Ressourcen eine harte Dependency-Kette.

Der schmalere, reine Bootstrap-Runbook lebt unter deploy/sw-auctions/runbooks/demo-bootstrap.md. Dieses Handbuch ist die breitere Operator-Sicht inkl. Teardown, Troubleshooting und Diagrammen.


2. Architektur auf einen Blick

Komponentenarchitektur

Komponentenarchitektur

KomponenteRolleHost (Demo)
storefront (Nuxt 3 SSR)öffentliche Auktions-UIshop.officina-del-caffe.uprix.de
shopware (PHP-FPM + Nginx)Hauptanwendung, Store-API + Adminadmin.officina-del-caffe.uprix.de
docs-server (VitePress)dieses Handbuchdocs.officina-del-caffe.uprix.de
erp-emulator (Nuxt 3)simuliert das ERPerp.officina-del-caffe.uprix.de
erp-integration (Go)Sync-Brücke ERP ↔ Shopwareclusterintern
minio (S3)Object-Store (Akten/Medien)clusterintern ⚠️ emptyDir
mailpitMailcatcher (DOI-/Outbid-Mails)mail.officina-del-caffe.uprix.de
RDS-MySQL 8Datenbank (OTC managed)privat
DCS-Redis 7Cache/Session (OTC managed)privat
SWRContainer-Registryswr.eu-de.otc.t-systems.com/officina-del-caffe

Verteilungsarchitektur

Verteilungsarchitektur

Die öffentliche IP gehört dem ELB (Load Balancer) vor dem CCE-Cluster — nicht einem Worker-Knoten. DNS zeigt per A-Record auf die ELB-EIP. Die Eltern-Domain uprix.de liegt bei united-domains; die OTC-Zone officina-del-caffe.uprix.de bekommt nach tofu apply per NS-Delegation ihre Nameserver zugewiesen.


3. Werkzeuge & Voraussetzungen

Lokal installiert (Tool-Boundary: nichts global nachinstallieren ohne Freigabe — bevorzugt Container/ddev):

ToolMindestensPrüfung
tofu (OpenTofu)1.8tofu version
kubectl1.29kubectl version --client
helm3.14helm version --short
jq1.6jq --version
python3 + PyYAML/Boto33.11python3 -c 'import yaml,boto3'
curl, opensslsystemcurl --version
age1.1age --version
docker24docker version

Ein gebündelter Preflight liegt im Taskfile:

cwd: Repo-Root ~/git/mms/odc/odc-tcloudenv: keine

bash
task ci:doctor

Beispiel-Output:

text
ci:doctor — tooling preflight
  ✅ docker       Docker version 24.0.7, build afdd53b
  ✅ helm         v3.14.2+gc309b6f
  ✅ python3      Python 3.11.9
  ✅ pyyaml       PyYAML 6.0.1
  ✅ tofu         OpenTofu v1.8.2
  ✅ task         Task version: v3.37.2
  ✅ kubectl      Client Version: v1.29.3
ci:doctor ok.

Zugänge, die Sie out-of-band brauchen:

  • Die age-Identity-Datei (privater Schlüssel) unter ~/.config/sw-auctions/age-identity.txt (oder via AGE_IDENTITY_FILE überschrieben). Sie liegt niemals im Repo.
  • Ein OTC-Account mit Web-Login für die Console.
  • Bitbucket-PATs (read + write) für den Image-Build-Pfad (nur beim Hochfahren nötig).
  • Zugang zum united-domains-Kundencenter für die NS-Delegation (nur beim ersten Mal bzw. nach einem Zonen-Neuaufbau).

4. Credentials laden (age-Bundle + OS_-Mapping)

Alle OTC-Credentials liegen age-verschlüsselt unter infra/connectivity/cred-bundles/<env>.env.age und werden mit secrets-load.sh in die aktuelle Shell geladen (Vertrag: ADR-008).

cwd: Repo-Root env: AGE_IDENTITY_FILE (nur falls Identity nicht am Default-Pfad liegt)

bash
# Bundle prüfen, OHNE etwas zu laden:
source infra/connectivity/secrets-load.sh --check

Beispiel-Output:

text
identity:      /home/stl/.config/sw-auctions/age-identity.txt (readable)
bundle-dir:    /home/stl/git/mms/odc/odc-tcloud/infra/connectivity/cred-bundles
  found: dev.env.age
managed vars:  OS_REGION_NAME OS_PROJECT_NAME ... OTC_TF_USERNAME OTC_TF_PASSWORD ...

Dann laden:

bash
source infra/connectivity/secrets-load.sh dev

⚠️ Fallstrick: OS_* vs. OTC_TF_* (Provider-Auth)

Der OpenTofu-opentelekomcloud-Provider liest OS_ACCESS_KEY / OS_SECRET_KEY und OS_USERNAME / OS_PASSWORD — nicht die OTC_TF_*-Namen und nicht die AWS_*-Variablen (die sind nur für das S3/OBS-State-Backend). Je nach Bundle-Stand exportiert secrets-load.sh teils nur OTC_TF_*. Verifizieren Sie nach dem Laden:

bash
env | grep -E '^OS_(ACCESS_KEY|SECRET_KEY|USERNAME|PASSWORD|REGION_NAME)=' \
  | sed 's/=.*/=<set>/'

Erwartet (Werte ausgeblendet):

text
OS_REGION_NAME=<set>
OS_ACCESS_KEY=<set>
OS_SECRET_KEY=<set>
OS_USERNAME=<set>
OS_PASSWORD=<set>

Fehlen OS_ACCESS_KEY/OS_SECRET_KEY, mappen Sie aus den OTC_TF_*:

bash
export OS_ACCESS_KEY="$OTC_TF_ACCESS_KEY"
export OS_SECRET_KEY="$OTC_TF_SECRET_KEY"
export OS_USERNAME="$OTC_TF_USERNAME"
export OS_PASSWORD="$OTC_TF_PASSWORD"

Wiedervorlage (bekannt): secrets-load.sh soll dieses Mapping künftig selbst erledigen. Bis dahin ist der Handgriff oben Pflicht — sonst scheitert tofu/Teardown mit Auth-Fehlern.

Aufräumen am Ende der Session:

bash
source infra/connectivity/secrets-load.sh --unset

5. Kontext-Konventionen (cwd, ENV, KUBECONFIG)

Damit immer klar ist, wo Sie agieren, gewöhnen Sie sich diesen Prompt-Banner an:

bash
echo "cwd=$(pwd)"; echo "KUBECONFIG=${KUBECONFIG:-<unset>}"; \
  kubectl config current-context 2>/dev/null || echo "context=<none>"

Beispiel-Output (gesunder Boot-Kontext):

text
cwd=/home/stl/git/mms/odc/odc-tcloud/infra/cce-appliance
KUBECONFIG=/home/stl/git/mms/odc/odc-tcloud/infra/cce-appliance/kubeconfig
context=swa-appliance

Die zentrale Variable ist KUBECONFIG. Sie zeigt nach dem Infra-Apply auf die vom Cluster exportierte kubeconfig:

bash
export KUBECONFIG="$(tofu -chdir=infra/cce-appliance output -raw kubeconfig_path)"

⚠️ Prüfen Sie vor jedem kubectl/helm-Kommando, dass current-contextswa-appliance ist — nicht versehentlich cce-nginx oder ein lokaler ddev-Kontext.


6. HOCHFAHREN — Schritt für Schritt

Arbeitssequenz Hochfahren

Gesamtmodell (ADR-025). Schritt 1 + 1b sind ein Bootstrap außerhalb des Clusters (Terraform + Argo-Install). Schritt 2–5 (Images → Deploy → Provisionieren → Smoke) laufen danach im Cluster. Für die manuelle Operator-Strecke fahren wir alle Schritte hier von Hand; die Pipeline-Automatisierung ist die spätere Ausbaustufe.

Schritt 0 — Credentials & Preflight

Siehe Kapitel 3 und 4. Ende dieses Schritts: task ci:doctor grün, OS_* gesetzt.

Schritt 1 — Infrastruktur (OTC) provisionieren

cwd: infra/cce-applianceenv: OS_* gesetzt (Schritt 0)

bash
cd infra/cce-appliance
tofu init
tofu plan -out plan.tfplan
tofu apply plan.tfplan        # CCE-Cluster, RDS-MySQL, DCS-Redis, OBS, DNS-Zone, VPC

Das legt an: CCE-Cluster swa-appliance (v1.29), RDS-MySQL 8, DCS-Redis 7, VPC/Subnet, DNS-Zone. Letzter gemessener Drill am 2026-06-10: 8m34s für den OpenTofu-Apply; für Operator-Planung weiter 10–15 min einrechnen.

⚠️ Replace-Falle: Zeigt tofu plan ein -/+ destroy and then create replacement für CCE-Worker (Tag-/Metadata-Konflikt), nicht apply. Stattdessen die Tags in der 🖱️ Console angleichen (siehe Kapitel 9, P-14) und neu planen.

kubeconfig abgreifen und Cluster prüfen:

bash
export KUBECONFIG="$(tofu output -raw kubeconfig_path)"
kubectl get nodes

Beispiel-Output:

text
NAME                STATUS   ROLES    AGE   VERSION
192.168.0.123       Ready    <none>   3m    v1.29.3
192.168.0.214       Ready    <none>   3m    v1.29.3

Schritt 1.1 — DNS-Delegation (nur erstmalig / nach Zonen-Neuaufbau)

bash
tofu output dns_zone_nameservers

Beispiel-Output:

text
dns_zone_nameservers = [
  "ns1.open-telekom-cloud.com.",
  "ns2.open-telekom-cloud.com.",
]

🖱️ Console / Registrar: Diese Nameserver im united-domains-Kundencenter als NS-Records für die Subdomain officina-del-caffe unter uprix.de eintragen.

⚠️ Bekannt (R6/R9): Die DNS-Zone hängt aktuell am cce-appliance-Workspace und wird beim tofu destroy mitgelöscht. Nach jedem Wiederaufbau zeigen die alten NS ins Leere — Delegation neu setzen. Mittelfristig wandert die Zone in einen eigenen, langlebigen infra/dns/-Workspace.

Schritt 1.2 — Plattform-Secrets in den Cluster

cwd: Repo-Root env: KUBECONFIG (Schritt 1), OS_*

bash
bash deploy/method-kit/scripts/render-platform-secrets.sh demo

Das liest die tofu-Outputs (mysql_*, redis_*) und schreibt idempotent die Secrets platform-mysql und platform-redis in den Namespace officina-demo.

Schritt 1.3 — Shopware-App- & JWT-Secrets (out-of-band)

⚠️ Diese Werte gehören NIE ins Repo. Der §6-Init bricht fail-fast ab, wenn sie fehlen.

bash
kubectl create namespace officina-demo 2>/dev/null || true

kubectl -n officina-demo create secret generic shopware-app \
  --from-literal=APP_SECRET="$(openssl rand -hex 16)" \
  --from-literal=JWT_PASSPHRASE=""

TMP="$(mktemp -d)"
openssl genrsa -out "$TMP/private.pem" 2048
openssl rsa -in "$TMP/private.pem" -pubout -out "$TMP/public.pem"
kubectl -n officina-demo create secret generic shopware-jwt \
  --from-file=private.pem="$TMP/private.pem" \
  --from-file=public.pem="$TMP/public.pem"
rm -rf "$TMP"

Frisch erzeugte JWT-Keys invalidieren alte Admin-Tokens — bei frischem Stage (leere RDS) unkritisch.

Schritt 1a — Ingress + cert-manager (Edge)

cwd: Repo-Root env: KUBECONFIG

bash
helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  -n ingress-nginx --create-namespace \
  --set controller.service.type=LoadBalancer \
  --set controller.service.annotations."kubernetes\.io/elb\.class"=union \
  --set-string controller.service.annotations."kubernetes\.io/elb\.autocreate"='{"type":"public","bandwidth_name":"swa-appliance-ingress","bandwidth_chargemode":"traffic","bandwidth_size":5,"bandwidth_sharetype":"PER","eip_type":"5_bgp"}'

OTC erzeugt dadurch automatisch einen ELB + EIP. EIP auslesen (kann 1–2 min dauern):

bash
INGRESS_EIP=$(kubectl -n ingress-nginx get svc ingress-nginx-controller \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "INGRESS_EIP=$INGRESS_EIP"

Beispiel-Output:

text
INGRESS_EIP=80.158.52.123

A-Record auf die EIP setzen (per Tofu-Var nachgereicht):

cwd: infra/cce-appliance

bash
tofu apply -var "ingress_eip=$INGRESS_EIP"

cert-manager + Let's-Encrypt-Issuer:

cwd: Repo-Root

bash
helm upgrade --install cert-manager cert-manager \
  --repo https://charts.jetstack.io \
  -n cert-manager --create-namespace \
  --set crds.enabled=true
kubectl apply -f deploy/method-kit/argo/bootstrap/letsencrypt-prod-issuer.yaml

⚠️ Let's-Encrypt-Ratelimit: Bei wiederholten Auf-/Abbauten am selben Tag riskieren Sie das LE-Production-Ratelimit (5 Zertifikate/Domain/Woche). Nutzen Sie für Drills den Staging-Issuer; wechseln Sie erst für die echte Demo auf letsencrypt-prod.

Schritt 1b — Argo (Workflows + ArgoCD)

cwd: Repo-Root env: KUBECONFIG, BITBUCKET_CI_READ_TOKEN, BITBUCKET_CI_WRITE_TOKEN, ARGO_ARTIFACT_BUCKET

bash
export ARGO_ARTIFACT_BUCKET='sw-auctions-argo-artifacts-dev'
task argo:bootstrap

deploy/sw-auctions/argo/bootstrap.sh legt den OBS-Artifact-Bucket bei einem From-scratch-Boot idempotent an, falls er noch fehlt. Nach dem Bootstrap sind die Tool-UIs direkt über die Demo-Domain erreichbar:

ToolURLHinweis
Argo Workflowshttps://argo.officina-del-caffe.uprix.deauthMode: server, kein separates Demo-Passwort im Runbook.
ArgoCDhttps://argocd.officina-del-caffe.uprix.deUser admin; Passwort aus dem Cluster-Secret abrufen, nicht dokumentieren.

ArgoCD-Initialpasswort sicher im Terminal abrufen:

bash
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d; echo

Schritt 2 — Images bauen (Argo-Workflow → SWR)

cwd: Repo-Root env: KUBECONFIG, BITBUCKET_CI_READ_TOKEN

Aktuellen main-HEAD ermitteln und Build-Workflow starten:

bash
REV=$(curl -sS -H "Authorization: Bearer $BITBUCKET_CI_READ_TOKEN" \
  "https://bitbucket.telekom-mms.com/rest/api/1.0/projects/BUDCCTA/repos/dccta-shopware-auctions/commits?until=refs/heads/main&limit=1" \
  | jq -r .values[0].id)
echo "REV=$REV"

cat <<YAML | kubectl create -f -
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata: {generateName: bootstrap-build-, namespace: officina-cicd}
spec:
  workflowTemplateRef: {name: service-build}
  arguments:
    parameters:
      - {name: repoUrl, value: "https://bitbucket.telekom-mms.com/scm/budccta/dccta-shopware-auctions.git"}
      - {name: revision, value: "$REV"}
      - {name: services, value: "[\"storefront\",\"shopware\"]"}
YAML

Workflow beobachten, bis Succeeded:

bash
kubectl -n officina-cicd get wf -w

Der Build pusht die Images nach SWR (...:demo-sha.<rev>) und bumpt deploy/sw-auctions/values/demo.yaml via git-commit-push.

Schritt 3 — Appliance per ArgoCD/ApplicationSet ausrollen

cwd: Repo-Root env: KUBECONFIG

Der Standardpfad ist GitOps: task argo:bootstrap appliziert das ApplicationSet officina-appliance; daraus entsteht die ArgoCD-Application officina-appliance-demo. Diese Application verwaltet den Helm-Release aus deploy/method-kit/charts/appliance mit deploy/sw-auctions/values/demo.yaml.

⚠️ Nicht zusätzlich manuell helm upgrade --install ausführen. Nach dem Argo-Bootstrap ist die App GitOps-owned; ein paralleler manueller Helm-Install kollidiert mit Ressourcen, die Argo bereits erzeugt hat.

Wenn der Build-Workflow neue Images gebaut und values/demo.yaml gepusht hat, Argo hart refreshen und auf den Remote-Commit warten:

bash
git fetch origin main
TARGET="$(git rev-parse origin/main)"

kubectl -n argocd annotate application officina-appliance-demo \
  argocd.argoproj.io/refresh=hard --overwrite

for i in $(seq 1 60); do
  STATUS="$(kubectl -n argocd get application officina-appliance-demo \
    -o jsonpath='sync={.status.sync.status} health={.status.health.status} rev={.status.sync.revision}{"\n"}')"
  echo "$STATUS"
  echo "$STATUS" | grep -q "sync=Synced health=Healthy rev=${TARGET}" && break
  sleep 10
done

kubectl -n argocd get application officina-appliance-demo -o wide

Erwartet: SYNC STATUS=Synced, HEALTH STATUS=Healthy, REVISION=$TARGET.

Debug-Fallback ohne Argo: Nur wenn argocd/ApplicationSet bewusst nicht installiert ist, darf der alte direkte Helm-Pfad genutzt werden:

bash
task render TAG="demo-sha.${REV:0:7}"
helm upgrade --install officina-appliance-demo \
  deploy/method-kit/charts/appliance \
  -n officina-demo -f deploy/sw-auctions/values/demo.yaml \
  --wait --timeout 20m

Dieser Fallback darf nicht parallel zur ArgoCD-Application officina-appliance-demo laufen.

Schritt 3.1 — Demo-Runtime-Artefakte (MinIO, Mailpit)

Diese Snapshots ergänzen die Helm-Chart um Dinge, die noch nicht im Chart sind:

bash
kubectl apply -f deploy/sw-auctions/demo-runtime/minio-deployment.yaml
kubectl apply -f deploy/sw-auctions/demo-runtime/minio-service.yaml
kubectl apply -f deploy/sw-auctions/demo-runtime/minio-init-job.yaml

kubectl apply -f deploy/sw-auctions/demo-runtime/mailpit-deployment.yaml
kubectl apply -f deploy/sw-auctions/demo-runtime/mailpit-service.yaml
kubectl apply -f deploy/sw-auctions/demo-runtime/mailpit-ingress.yaml

Shopware darf hier nicht gepatcht werden: Das Prod-Image startet supervisord mit nginx + php-fpm. Ein php -S-Command-Override ist eine SPEC-023-Regression und führt zu falschem Media-/Admin-Routing.

⚠️ ArgoCD-Self-Heal: Solange Sie Live-Patches fahren, überschreibt ArgoCD sie sofort wieder. Im Demo-Modus die Controller nur fuer die Dauer eines bewusst dokumentierten Live-Fix abschalten:

bash
kubectl -n argocd scale statefulset argocd-application-controller --replicas=0
kubectl -n argocd scale deploy argocd-applicationset-controller --replicas=0

Nach Commit/Push des Desired-State-Fix die Controller sofort wieder hochfahren und auf den gepushten Commit reconciliieren:

bash
TARGET=$(git rev-parse HEAD)
kubectl -n argocd scale statefulset argocd-application-controller --replicas=1
kubectl -n argocd scale deploy argocd-applicationset-controller --replicas=1
kubectl -n argocd rollout status statefulset/argocd-application-controller --timeout=180s
kubectl -n argocd rollout status deploy/argocd-applicationset-controller --timeout=180s
kubectl -n argocd annotate application officina-appliance-demo \
  argocd.argoproj.io/refresh=hard --overwrite
kubectl -n argocd get application officina-appliance-demo -o wide

Erwartet: SYNC STATUS=Synced, HEALTH STATUS=Healthy, REVISION=$TARGET.

Der minio-init-Job legt bvg-records mit Object-Lock COMPLIANCE/1d an. Falls ein leerer Demo-Bucket ohne Object-Lock existiert, wird er neu angelegt; bei nicht-leerem Bucket bricht der Job bewusst ab.

Schritt 4 — Shopware provisionieren (wie ddev)

cwd: Repo-Root env: KUBECONFIG

bash
SW=$(kubectl -n officina-demo get pods -l app.kubernetes.io/name=shopware \
       -o jsonpath='{.items[?(@.status.phase=="Running")].metadata.name}')
echo "SW=$SW"

kubectl -n officina-demo exec $SW -c shopware -- bash -lc '
  cd /var/www/html
  php bin/console system:install --basic-setup --force --create-database --no-interaction
  touch install.lock

  php bin/console plugin:refresh
  for p in SwAuctionsServiceNotification SwAuctionsServiceCustomer \
           SwAuctionsServiceCatalogue   SwAuctionsServiceRecords \
           SwAuctionsServiceAuction     SwAuctionsServiceBidding; do
    php bin/console plugin:install --activate --no-interaction $p || true
  done

  php bin/console assets:install
  php bin/console theme:compile
  php bin/console dal:refresh:index

  php bin/console system:config:set SwAuctionsServiceRecords.config.s3Endpoint     "http://minio:9000"
  php bin/console system:config:set SwAuctionsServiceRecords.config.s3Region       "us-east-1"
  php bin/console system:config:set SwAuctionsServiceRecords.config.s3AccessKey    "minioadmin"
  php bin/console system:config:set SwAuctionsServiceRecords.config.s3SecretKey    "minioadmin"
  php bin/console system:config:set SwAuctionsServiceRecords.config.s3Bucket       "bvg-records"
  php bin/console system:config:set SwAuctionsServiceRecords.config.s3UsePathStyle "1"

  php bin/console sw-auctions:catalogue:seed-coffee-lots
  php bin/console sw-auctions:auction:seed-demo-fanout
'

Schritt 4.1 — Sales-Channel-Domains setzen

⚠️ Bekannt: Shopware braucht je Sales-Channel mindestens einen Domain-Eintrag. Fehlt er, zeigt der Browser „Sales Channel Not Found". system:install --basic-setup legt nur http://localhost an; das muss auf shop.* (und für den Headless/Admin-Channel auf admin.*) gemappt werden.

bash
kubectl -n officina-demo exec $SW -c shopware -- php -r "
  preg_match('#mysql://([^:]+):([^@]+)@([^:]+):(\d+)/(.+)#', getenv('DATABASE_URL'), \$m);
  \$p = new PDO(\"mysql:host={\$m[3]};port={\$m[4]};dbname={\$m[5]}\", \$m[1], \$m[2]);
  \$p->exec('UPDATE sales_channel_domain SET url=\"https://shop.officina-del-caffe.uprix.de\" WHERE url LIKE \"http://localhost%\"');
  echo 'shop-domain rows: ' . \$p->query('SELECT ROW_COUNT()')->fetchColumn() . PHP_EOL;
"

Schritte 5–8 — Smoke-Tests / Live-Bestätigung

Siehe Kapitel 11. Ende: alle App- und Tool-Domains liefern HTTP 200, die Store-API liefert total=7, und sw-auctions:auction:list zeigt 7 Auktionen. Die Appliance ist LIVE.


7. HERUNTERFAHREN — geordneter Abriss

Arbeitssequenz Herunterfahren

Grundregel (teuer gelernt). Die Tofu-State-Liste ist KEINE verlässliche Quelle dafür, „was noch lebt". Verwaiste Ressourcen (RDS-Instanz, Security-Group-v1, EVS-Disks) überleben ein tofu destroy und müssen über direkte OTC-APIs plus Console-RMS-Inventur gegengeprüft und in der richtigen Reihenfolge gelöscht werden.

Phase A — Workload abräumen (zuerst, im Cluster!)

cwd: Repo-Root env: KUBECONFIG

bash
export KUBECONFIG="$PWD/infra/cce-appliance/kubeconfig"

# ArgoCD-Application zuerst (sonst re-synct sie alles zurück)
kubectl -n argocd delete application officina-appliance-demo \
  --cascade=foreground --wait=true --ignore-not-found=true

# Helm-Releases
helm -n officina-demo uninstall officina-appliance-demo || true
helm -n officina-demo uninstall officina || true

# Namespace (löst Pods, Services, PVCs)
kubectl delete namespace officina-demo --wait=true

⚠️ LoadBalancer zuerst freigeben: Jeder Service: type=LoadBalancer hält einen ELB + EIP in OTC. Werden diese nicht über das Löschen der Services freigegeben, hängen ELB/EIP später beim tofu destroy. Das Löschen des Namespace erledigt das für die App-Services; den ingress-nginx-Controller separat:

bash
helm -n ingress-nginx uninstall ingress-nginx || true
kubectl delete namespace ingress-nginx --wait=true

PVCs der CI-Workspaces (Argo) lösen die zugehörigen EVS-Volumes:

bash
kubectl -n officina-cicd delete pvc --all --wait=true || true

Optional, wenn Argo selbst auch weg soll:

bash
kubectl delete namespace officina-cicd --wait=true --ignore-not-found=true --timeout=10m
kubectl delete namespace argocd --wait=true --ignore-not-found=true --timeout=10m

Wenn argocd dabei in Terminating hängen bleibt und die Namespace-Conditions resources-finalizer.argocd.argoproj.io auf einer Application zeigen, ist der Application-Controller wahrscheinlich im Demo-Modus auf 0 skaliert und kann den Finalizer nicht selbst entfernen. Dann gezielt nur diesen ArgoCD-Finalizer löschen:

bash
kubectl -n argocd patch application officina-appliance-demo \
  --type=merge -p '{"metadata":{"finalizers":null}}' || true
kubectl wait --for=delete namespace/argocd --timeout=5m || true

Phase B — Infrastruktur per Tofu zerstören

cwd: infra/cce-applianceenv: OS_* gesetzt und verifiziert (Kapitel 4!)

bash
cd infra/cce-appliance
tofu destroy

Das zerstört regulär ~20 Ressourcen: CCE-Cluster, NodePool, NAT, beide EIPs, VPC+Subnet, RDS, DCS, DNS-Zone + Wildcard, SecGroups, Keypair.

⚠️ Kein endloses Retry. Zeigt der Lauf minutenlang Still destroying... (typisch für Subnet + RDS-SecGroup), maximal einen Retry — danach in Phase C mit einer OTC-API-Inventur weitermachen. Endloses Warten löst nichts.

Phase C — Verwaiste Reste manuell abräumen (Dependency-Kette)

Statussequenz Ressourcen-Abriss

🖱️ Console: Öffnen Sie das RMS (Resource Management Service) und machen Sie einen Ressourcen-Screenshot/-Export. Das zeigt Ressourcen, die außerhalb des Tofu-State leben. Achtung: Der Console-RMS-Index kann stale sein (zeigt z. B. einen CCE-Cluster, der per Direkt-API schon 404 ist) — die Direkt-API ist die Wahrheit.

Lösch-Reihenfolge (hart): RDS → SecurityGroup(v1) → EVS-Disks → Subnet → VPC. Jede andere Reihenfolge endet mit 409 („Subnet still in use" / „Router contains subnets").

  1. Verwaiste RDS-Instanz (z. B. swa-appliance-mysql) über die RDS-v3-API löschen. 🖱️ Console-Alternative: Relational Database Service → Instanzen → Löschen.
  2. Security-Group des RDS löschen. ⚠️ API-Quirk: Die Neutron-v2.0-API meldet die SG fälschlich als 404, während sie real noch existiert und der echte Router-Blocker ist. Löschung MUSS über die v1-native VPC-API erfolgen: DELETE /v1/{project}/security-groups/{id} (→ 204404). 🖱️ Console-Alternative: VPC → Security Groups (nicht Network Console → Security Groups, das ist die Neutron-Sicht).
  3. Verwaiste EVS-Disks (pvc-*, Status available, 0 Attachments) löschen. 🖱️ Console: Elastic Volume Service → Disks → Status „Available" → Löschen.
  4. Subnet löschen (jetzt ohne 409). 🖱️ VPC → Subnets.
  5. VPC löschen. 🖱️ VPC → Virtual Private Cloud.

Tofu-State nachziehen (entfernt die nicht mehr existierenden Objekte aus dem State):

bash
tofu state list
tofu state rm 'opentelekomcloud_vpc_subnet_v1.this' 'opentelekomcloud_vpc_v1.this'
# nur die Objekte entfernen, die per API bereits 404 sind!

Phase D — Verifikation (unabhängiger Scan)

🖱️ Optional per Console-RMS, verlässlicher per IAM-Token + REST-APIs. Ziel ist ein Gesamt-Scan = 0 über alle App-Ressourcenklassen:

RessourceSoll
CCE-Cluster0
RDS0
DCS/Redis0
VPC0
Subnet0
SecurityGroup0
EIP0
ELB0
NAT-GW0
EVS-Volumes0
DNS-Zonen0
SWR-Namespaces0 (falls auch SWR abgerissen wird — meist behalten)

⚠️ DNS-Konsequenz: Nach dem Abriss existiert die Zone officina-del-caffe.uprix.de. nicht mehr; die NS-Delegation bei united-domains zeigt ins Leere, bis ein Re-Apply neue OTC-Nameserver liefert und Sie die Delegation neu setzen (Schritt 1.1).


8. Statussequenzen

Appliance-Lifecycle (Operator-Sicht)

Statussequenz Appliance-Lifecycle

Der grüne Zustand Live ist das Ziel für die SCD-Demo. Von dort führen drei Pfade zurück: ein Pod-Recreate (Medien gehen verloren → Reseed), ein Image-Bump (ArgoCD-Sync) und der Teardown (Workload weg → Abriss → Leer).

OTC-Ressourcen-Abriss (Dependency-Kette)

Siehe das Diagramm in Phase C. Es zeigt die harte Reihenfolge RDS → SG(v1) → EVS → Subnet → VPC und die typischen 409-Blocker dazwischen.


9. Bekannte Probleme & Lösungen

Diese Tabelle ist die destillierte Erfahrung aus den bisherigen Boot- und Teardown-Sessions. Quelle steht je Zeile dabei.

#SymptomUrsacheLösungQuelle
P-1tofu destroy hängt minutenlang auf Still destroying... (Subnet, RDS-SG)OTC-Delete-Latenz / verwaiste AbhängigkeitMax. 1 Retry, dann API-Inventur (Phase C). Kein endloses Warten.SESSION-2026-05-27_18-40
P-2Nach tofu destroy leben RDS/SG/EVS weiterNicht alles im Tofu-State; Reste überleben DestroyDirekt per OTC-API + RMS prüfen und in Reihenfolge RDS→SG(v1)→EVS→Subnet→VPC löschenSESSION-2026-05-30_18-20
P-3409 "Router contains subnets" / "Subnet still in use"Falsche Lösch-ReihenfolgeErst RDS, dann SG(v1), dann EVS-Disks, dann Subnet, dann VPCSESSION-2026-05-30_18-20
P-4Security-Group meldet via API 404, blockiert aber trotzdemNeutron-v2-API lügt; SG existiert in v1-VPC-APILöschen über DELETE /v1/{project}/security-groups/{id} (Console: VPC → Security Groups)SESSION-2026-05-30_18-20
P-5DNS-Zone nach Destroy weg, Domain totZone hängt am cce-appliance-WorkspaceNS-Delegation nach Re-Apply neu setzen; mittelfristig eigener infra/dns/-WorkspaceSESSION-2026-05-28_18-14
P-6Ingress nicht erreichbar, DNS zeigt auf Worker-IPPublic IP gehört dem ELB, nicht dem WorkerA-Record auf ELB-EIP; Service type=LoadBalancer erzeugt ELB+EIPSESSION-2026-05-28_00-59
P-7Root-Domain lässt sich nicht per CNAME auf ELB legenCNAME auf Root ist DNS-technisch unzulässigA-Record direkt auf die ELB-EIPSESSION-2026-05-28_00-59
P-8TLS/HTTPS fehlt am Ingresscert-manager + Issuer nicht installiertingress-nginx und cert-manager + LE-Issuer per Helm ausrollen (Schritt 1a)SESSION-2026-05-28_00-59
P-9Manuelle Patches verschwinden nach SekundenArgoCD-Self-Heal reconciled zurückController nur kurz auf replicas=0; Fix in Chart/Manifest/Values ziehen, committen/pushen, Controller wieder auf 1 und Synced/Healthy prüfenSESSION-2026-05-27_18-40, SESSION-2026-06-10_12-43
P-10Medien/Bilder weg nach Pod-RecreateMinIO nutzt emptyDir statt PVCReseed: sw-auctions:catalogue:seed-coffee-lots --force-media-reimport; produktiv PVCSESSION-2026-05-28_18-14
P-11Storefront-Bilder 404 (/products/<hersteller>/...)Altes Storefront-Image oder stale Daten referenzieren Bildpfade, die im aktuellen Build nicht vorhanden sindStorefront-Image neu bauen/pushen und Demo-Daten reseeden; das aktuelle Image enthält Routen und Assets ohne Nitro-Live-PatchSESSION-2026-05-28_18-14, SESSION-2026-06-02_18-08
P-12Browser zeigt „Sales Channel Not Found"Sales-Channel ohne Domain-EintragDomains auf shop.*/admin.* mappen (Schritt 4.1)demo-bootstrap.md
P-13Images nicht push-/pullbarSWR-Org/Repos fehlen oder Pull-Secret fehltSWR-Org officina-del-caffe + Repos anlegen; swr-pull-Secret im NamespaceSESSION-2026-05-28_18-14
P-14tofu plan will CCE-Worker replacen (-/+)Tag-/Metadata-KonfliktNicht applyen; in der Console re-taggen, dann neu planenSESSION-2026-05-27_15-48
P-15ELB-Tagging/Ownership unklarELB entsteht out-of-Tofu via Service: LoadBalancerkubernetes.io/elb.tags-Annotation am Service setzenSESSION-2026-05-27_15-48
P-16tofu/Teardown scheitert mit Auth-FehlerProvider braucht OS_*, nicht OTC_TF_*/AWS_*OS_ACCESS_KEY/OS_SECRET_KEY/OS_USERNAME/OS_PASSWORD mappen (Kapitel 4)SESSION-2026-05-28_18-14
P-17aws/aws-sdk-php fehlt im Pod (vendor/aws)Lag früher in require-dev statt requireProd-Image/Composer-Lock korrigieren; kein Runtime-composer require per Bootstrap-CMSESSION-2026-05-28_18-14, SPEC-023
P-18Shopware bootet nicht / Web-Installer erscheintinstall.lock fehltinstall.lock ins Prod-Image backen; kein Bootstrap-Skript als Webserver-ErsatzSESSION-2026-05-28_18-14, SPEC-023
P-19RollingUpdate erzeugt 2. Pod, sprengt knappes ClusterSurge-Strategie bei 1-Replicastrategy.type=Recreate (im Manifest gesetzt)SESSION-2026-05-30_18-20
P-20Admin-Host-Root landet in Storefront-Channel-Auflösung/ nicht nach /admin geführtIngress-Annotation nginx.ingress.kubernetes.io/app-root=/adminSESSION-2026-05-30_18-20
P-21Shopware-Init endet mit There are no commands defined in the "sw-auctions:storefront" namespaceGepinntes Shopware-Image ist älter als der Init-Vertrag und enthält sw-auctions:storefront:provision-cms noch nichtInit guardiert den Command per bin/console list; langfristig neues Shopware-Image bauen/pinnenSESSION-2026-06-05_00-06
P-22Argo-Bootstrap bricht lokal mit yq (v4) required abOperator-Maschine hat kein lokales yq; Tool-Boundary verbietet Nachinstallationdeploy/sw-auctions/argo/bootstrap.sh nutzt Python/PyYAML für Template-PatchesSESSION-2026-06-05_00-06
P-23infra/swr-Plan will erp-emulator/erp-integration zerstörenSWR-IaC-Liste war älter als die Demo-ValuesRepositories in infra/swr/variables.tf dauerhaft aufnehmen; SWR-Plan muss 0-Destroy seinSESSION-2026-06-05_00-06
P-24Runbook nennt demo.env.age, Preflight findet nur dev.env.ageRealer Credential-Bundle-Name ist aktuell devsource infra/connectivity/secrets-load.sh dev verwenden; kein Klartext-Bundle anlegenSESSION-2026-06-05_00-06
P-25argocd-Namespace bleibt beim Teardown in TerminatingDemo-Modus hat den ArgoCD-Application-Controller auf 0 skaliert; Application-Finalizer kann nicht abgeräumt werdenNur den betroffenen resources-finalizer.argocd.argoproj.io auf der Demo-Application patchen, dann Namespace-Wait fortsetzenSESSION-2026-06-05_00-06
P-26Admin lädt nicht sauber oder Storefront-Bilder liefern Sales Channel Not Found/400 auf admin.*Shopware-Deployment wurde per Runtime-Patch auf php -S ... public/index.php umgebogen; vorhandene Media-/Asset-Pfade laufen dann durch Shopware statt nginxCommand-/Bootstrap-Patch entfernen; Pod muss Image-CMD supervisord mit nginx + php-fpm nutzen; aktuelle Media-URLs nach Pod-Recreate neu prüfenSESSION-2026-06-10_12-43

Jeder geschlossene Root-Cause (R1R9 im Runbook) verkleinert den manuellen Aufwand. Den aktuellen Stand der offenen Punkte führt deploy/sw-auctions/runbooks/demo-bootstrap.md im Abschnitt „Root-cause TODOs".


10. OTC-Console-Cheatsheet

Login: https://console.otc.t-systems.com → Region eu-de wählen (oben).

Ich will…In der Console…
Cluster-Status sehenCloud Container Engine (CCE) → Cluster
RDS-Instanz löschenRelational Database Service → Instanzen
Redis löschenDistributed Cache Service (DCS)
Security-Group löschen (echte Quelle)Virtual Private Cloud → Security Groups ⚠️ nicht die Network-Console-Sicht
Verwaiste Disks findenElastic Volume Service (EVS) → Disks → Filter „Available"
EIP/ELB prüfenElastic IP bzw. Elastic Load Balance
NAT-Gateway prüfenNAT Gateway
DNS-Zone & RecordsDomain Name Service → Public Zones
Nameserver für DelegationDNS → Zone öffnen → NS-Record (gehört zu united-domains)
Registry/ImagesSoftWare Repository for Container (SWR)
State-/Artefakt-BucketsObject Storage Service (OBS)
Gesamt-Inventur (kann stale sein)Resource Management Service (RMS) → Resources

⚠️ Console-Disziplin (ADR-008): Jede Console-Änderung gehört in die jeweilige SESSION-…-Datei vermerkt und in infra/-Tofu nachgezogen. Keine Console-Klicks ohne IaC-Nachzug.


11. Health- & Smoke-Checks

cwd: Repo-Root env: KUBECONFIG

HTTP-Smoke (Live-URLs):

bash
for u in https://shop.officina-del-caffe.uprix.de/ \
         https://shop.officina-del-caffe.uprix.de/auctions \
         https://admin.officina-del-caffe.uprix.de/admin \
         https://docs.officina-del-caffe.uprix.de/ \
         https://erp.officina-del-caffe.uprix.de/ \
         https://mail.officina-del-caffe.uprix.de/ \
         https://argo.officina-del-caffe.uprix.de/ \
         https://argocd.officina-del-caffe.uprix.de/; do
  curl -sS -o /dev/null -w "$u → %{http_code}\n" "$u"
done

Beispiel-Output (gesund):

text
https://shop.officina-del-caffe.uprix.de/ → 200
https://shop.officina-del-caffe.uprix.de/auctions → 200
https://admin.officina-del-caffe.uprix.de/admin → 200
https://docs.officina-del-caffe.uprix.de/ → 200
https://erp.officina-del-caffe.uprix.de/ → 200
https://mail.officina-del-caffe.uprix.de/ → 200
https://argo.officina-del-caffe.uprix.de/ → 200
https://argocd.officina-del-caffe.uprix.de/ → 200

Store-API-Smoke:

bash
curl -sS \
  -H 'sw-access-key: SWSCBFFRZZM3TFLUT3PFQWR1ZW' \
  https://admin.officina-del-caffe.uprix.de/store-api/sw-auctions/auctions

Erwartet: HTTP 200 mit total=7 und 7 elements.

Fachlicher Check (Shopware-CLI):

bash
SW=$(kubectl -n officina-demo get pods -l app.kubernetes.io/name=shopware \
       -o jsonpath='{.items[?(@.status.phase=="Running")].metadata.name}')
kubectl -n officina-demo exec $SW -c shopware -- php bin/console sw-auctions:auction:list

Erwartet: 7 Auktionen mit 40 Listings.

Pod-Übersicht:

bash
kubectl -n officina-demo get pods

Beispiel-Output (gesund):

text
NAME                                              READY   STATUS    RESTARTS   AGE
officina-appliance-demo-shopware-...              1/1     Running   0          12m
officina-appliance-demo-storefront-...            1/1     Running   0          12m
officina-appliance-demo-erp-emulator-...          1/1     Running   0          12m
officina-appliance-demo-erp-integration-...       1/1     Running   0          12m
minio-...                                         1/1     Running   0          14m
mailpit-...                                        1/1     Running   0          11m

12. Was bewusst bestehen bleibt

Beim regulären Appliance-Teardown werden diese Dinge absichtlich NICHT mitgelöscht:

  • OBS-Bucket sw-auctions-tfstate-dev — das gemeinsame Tofu-Remote-State-Backend. Ohne ihn verlieren alle Workspaces ihren State.
  • SWR-Org officina-del-caffe (Repos storefront, shopware, docs-server, erp-emulator, erp-integration) — Image-Cache für schnelles Re-Apply. Liegt im getrennten Workspace infra/swr/.
  • OBS-Bucket sw-auctions-argo-artifacts-dev — Argo-Workflow-Artefakte. Wird vom Argo-Bootstrap bei Bedarf angelegt und gehört nicht zum Appliance-Teardown.
  • Workspaces infra/connectivity/, infra/cce-nginx/, infra/mcp-server/ — nicht Teil des Appliance-Abrisses; ihr State liegt ebenfalls in sw-auctions-tfstate-dev.

Wer auch SWR abreißen will (selten), nutzt cd infra/swr && tofu destroy separat — und nimmt den 15-min-Layer-Re-Push beim nächsten Aufbau in Kauf.


Verwandte Spezifikationen & Quellen

  • Runbook deploy/sw-auctions/runbooks/demo-bootstrap.md — schmaler Bootstrap-Pfad.
  • ADR-025 — In-Cluster-Bootstrap-Pipeline (Schritt 2–5 als Argo-DAG).
  • ADR-010 — OpenTofu/OTC-Stack & OBS-State.
  • ADR-013 — Argo-Family (CD-Trigger-Vertrag).
  • ADR-008 — Connectivity / age-Bundle / Console-Disziplin.
  • ADR-015 — Resource-Tagging-Convention.
  • infra/cce-appliance/README.md — Workspace-Scope & Variablen.
  • infra/connectivity/README.md — Cred-Bootstrap.
  • Sessions: SESSION-2026-05-28_18-14-tcloud-live.md, SESSION-2026-05-30_18-20-tcloud-provisioning-patterns.md, SESSION-2026-05-27_18-40-argo-family-poc-otc-and-anti-retry-runbook.md, SESSION-2026-05-28_00-59-recherche-cce-ingress-domains-otc.md, SESSION-2026-06-05_00-06-tcloud-bringup-after-provisioning-changes.md.

Changelog

VersionDatumAutor/RolleÄnderungBegründung / Trigger
0.1.02026-05-31infra-engineerErstfassung des Operator-Handbuchs T-Cloud-Lifecycle: Hoch-/Herunterfahren, 6 PlantUML/SVG-Diagramme (Komponenten-, Verteilungsarchitektur, Boot-/Teardown-Sequenz, 2 Statussequenzen), bekannte Probleme P-1…P-20, OTC-Console-Cheatsheet, Smoke-Checks.Orchestrator-Auftrag: vollständige, agentenfreie Operator-Anleitung in den Handbüchern, als SSOT-Derivat und im Docs-Server.
0.1.12026-06-02infra-engineerminio-init-job.yaml in die Demo-Runtime-Artefakte aufgenommen; manueller Bucket-Befehl durch Job-Vertrag ersetzt.T-Cloud-Re-Bootstrap zeigte, dass der Records-Bucket ohne Object-Lock erzeugt wurde und seed-coffee-lots deshalb beim S3-Upload scheiterte.
0.1.22026-06-02infra-engineerObsoleten Storefront-Nitro-Live-Patch aus dem Runtime-Apply-Set entfernt.Neues Storefront-Image enthält die Routes/Assets selbst; der alte Patch scheiterte unter USER node an root-owned .output.
0.1.32026-06-05infra-engineerRealen From-scratch-Bootpfad nachgezogen: dev-Credential-Bundle, OTC-ELB-Autocreate, Argo-Tool-Domains, Argo-Artifact-Bucket, yq-freier Bootstrap, SWR-Repos, Shopware-CMS-Init-Guard, Smoke-Erwartung 7 Auktionen.Boot-Drill nach Provisionierungsänderungen zeigte mehrere reproduzierbarkeitsrelevante Lücken.
0.1.42026-06-05infra-engineerTeardown-Pfad um Argo-Namespace-Abriss und ArgoCD-Finalizer-Recovery ergänzt.Session-Ende mit explizitem Teardown zeigte einen argocd-Namespace in Terminating, weil der Demo-Self-Heal-Controller auf 0 skaliert war.
0.1.52026-06-10infra-engineerShopware-Bootstrap-CM und Runtime-Patch aus dem Hochfahrpfad entfernt; Known-Issue P-26 für php -S-Regression ergänzt.T-Cloud-Debug: Admin-/Media-400 durch Rückfall auf den verbotenen Built-in-Server-Pfad.
0.1.62026-06-10infra-engineerArgoCD-Re-enable und Synced/Healthy-Abschlusscheck ergänzt; P-9 präzisiert.Nach Commit 1175a9b reconciliierte die Demo-App stabil auf den Remote-Desired-State.
0.1.72026-06-10infra-engineerBootpfad auf GitOps-Standard korrigiert: ApplicationSet/ArgoCD ist führend; direkter Helm-Install ist nur noch Debug-Fallback ohne Argo. Gemessene Provisioning-Zeit und Python/Boto3-Voraussetzung ergänzt.Doku-Drift-Check nach stabilem Boot/Teardown: manueller Helm-Install kollidiert nach task argo:bootstrap mit GitOps-owned Ressourcen.
0.1.82026-06-10spec-stewardKnown-Issue P-11 vom gelöschten Storefront-Nitro-Live-Patch auf aktuellen Image-/Reseed-Vertrag korrigiert.Doku-Drift-Check: storefront-patches-ConfigMap existiert nicht mehr; aktuelle Storefront-Images enthalten Routen und Assets selbst.