Prod Cutover Checklist¶
Offene Aufgaben, die beim Schritt von Staging auf den produktiven
web.targetshot.app-Deploy mit erledigt werden müssen. Neue Items bitte
hier unten anhängen, abgeschlossene abhaken statt zu löschen — dient
als Audit-Spur.
Azure Blob Storage: Prod-Cleanup-Workflow¶
Erledigt am 2026-09-01 — aber anders als unten geplant: Prod läuft als
App Service (kein Compose, kein self-hosted Runner), deshalb startet
cleanup-orphan-photos-prod.yml einen Einmal-Container (ACI) in der
RG targetshot-jobs mit dem aktuell deployten Backend-Image und führt
das Skript dort aus. Secrets kommen zur Laufzeit aus kc-kv-prd01
(gh-oidc-SP ist Key Vault Secrets User; Contributor nur auf
targetshot-jobs), die DB ist über die bestehende
„Allow Azure services"-Firewallregel erreichbar.
- [x] Workflow
cleanup-orphan-photos-prod.ymlangelegt (ACI-Variante, siehe oben — die ursprünglich geplanten Punkte Prod-Runner,PROD_DOCKER_COMPOSE_DIRund Secret-Injektion in den Deploy entfallen damit ersatzlos). - [x] Einmaliger Dry-Run am 2026-09-01 (per identischem ACI-Rezept): 2 Blobs insgesamt, 0 Orphans — Prod ist sauber; der erste echte Durchlauf lohnt erst nach ein paar Foto-Updates.
- [x] Keine Shared-Container-Gefahr: eigene Container je Umgebung
(
photos-stagingvs.photos-prod); der Cleanup diff't nur gegen die DB seinesDATABASE_URL. Zusätzlich gehärtet am 2026-09-01: das Skript zieht die Blob-Liste vor dem DB-Lesen (Wächter-Testbackend/test/cleanupOrphanPhotosOrder.test.ts), damit ein Upload während des Laufs nicht als Waise gilt.
Keycloak: Prod-Deployment nach Azure¶
Design-Dokument: Keycloak auf Azure.
Staging bleibt unverändert on-prem hinter HAProxy; der Cutover betrifft
nur auth.targetshot.app + auth-admin.targetshot.app.
Infrastruktur¶
Status 2026-04-26 (Sprint 19 #104): Provisioniert via
infrastructure/keycloak/main.bicep+deploy.sh. Endpunkt: https://keycloak-prod-prd01.azurewebsites.net (Default-Hostname, Custom Domain folgt unten). Liquibase-Migration lief beim ersten Container-Boot durch —/realms/masterantwortet 200. Abweichungen ggü. ursprünglichem Design sind unten markiert (*Δ*).
- [x] Resource Group
targetshot-kc(Germany West Central). - [x] VNet
targetshot-kc-vnet(10.20.0.0/16) mit Subnetapp(10.20.1.0/24), delegated zuMicrosoft.Web/serverFarms. - [x] Δ
db-Subnet entfällt — Postgres läuft mit **Public Endpoint - Firewall-Regel
AllowAllAzureServicesAndResourcesWithinAzureIps** statt private VNet-Integration. Grund:LocationIsOfferRestrictedfür Flex-Server in Germany West Central in unserer Subscription. Postgres läuft daher inwesteurope(übriges Stack bleibt in Germany West Central). SSL ist erzwungen, Whitelist begrenzt Zugriff auf Azure-IPs. - [x] Δ Private DNS Zone entfällt (folgt aus dem Punkt darüber).
- [x] Azure Container Registry Basic (
targetshotkcprd01). - [x] Δ Azure DB for PostgreSQL Flex
Standard_B1msinwesteurope, Server-Nametargetshot-kc-pg-prd01-we, Admin-Userpgadmin, 32 GB SSD, 7-Tage PITR. Public Access (s. o.). - [x] Δ App Service Plan Linux
P0v3(PremiumV3) statt B2 — unsere Subscription hatte für die Basic-Familie Limit=0; Quota via Microsoft.Quota für P0v3 gehoben. Kostenlich vergleichbar. - [x] Web App
keycloak-prod-prd01, Container aus ACR, System- Assigned Managed Identity zugeordnet (AcrPull + KV-Secret-User). - [x] Regional VNet Integration der Web App auf Subnet
app. - [x] Key Vault
kc-kv-prd01für DB-Passwort, Web App holtKC_DB_PASSWORDvia@Microsoft.KeyVault(...)Reference (Status peraz rest /configreferences: Resolved).
Datenbank-Bootstrap¶
- [x]
keycloak-Datenbank wird durch Bicep (Microsoft.DBforPostgreSQL/flexibleServers/databases) angelegt. Schema-Migration übernimmt Liquibase beim ersten Keycloak-Start; manuellesCREATE USER/GRANTentfällt, weil Keycloak mit dem Server-Admin (pgadmin) connected. - [x] Passwort in Key Vault unter
kc-db-password(vomdeploy.shbeim ersten Lauf generiert; Re-Runs lesen es zurück).
Keycloak-Image¶
- [x]
infrastructure/keycloak/Dockerfile(Keycloak 26.0.7 LTS, Themes baked-in,kc.sh buildim Builder-Stage,start --optimizedals CMD). - [x] Workflow
infrastructure/.github/workflows/build-keycloak-image.ymlbaut + pusht bei Push aufkeycloak/**nachtargetshotkcprd01.azurecr.io/keycloak:<sha>. Pre-conds: Federated Credential fürrepo:targetshot-app/infrastructure: ref:refs/heads/main+ AcrPush-Rolle auf der ACR sind angelegt. - [ ] Optionaler
deploy-keycloak-prod.yml(rollt neue Tags automatisch viaaz webapp config container setaus). Aktuell manuell — passt für die niedrige Release-Frequenz.
Domains¶
- [x] Cloudflare DNS: CNAME
auth→keycloak-prod-prd01.azurewebsites.net, DNS only (graue Wolke). - [x] Cloudflare DNS: CNAME
auth-admin→ gleicher Target, DNS only. - [x] Azure App Service Custom Domain
auth.targetshot.appgebunden (CNAME-Verifikation reichte, kein TXT nötig). Managed Certificate viaaz webapp config ssl create+ SNI-Binding aktiv (GeoTrust, läuft bis 2026-10-26). - [x] Gleiche Prozedur für
auth-admin.targetshot.app. - [x]
KC_HOSTNAMEaufhttps://auth.targetshot.appumgestellt (Keycloak baut Cookies + OIDC-URLs jetzt mit dem Custom-Hostname).
Admin-Console-Härtung (Host-Split + 2FA)¶
Status 2026-04-26: Microsoft hat Path-based App-Service- Restrictions entfernt — weder ARM/Bicep/REST (
pathPatternnicht im Schema) noch das Portal (kein Path-Feld im "Add rule"-Dialog) bieten das mehr an. Statt IP-Allowlist auf/admin/*setzen wir auf zwei billigere Schichten:
- [x] Host-Split via
KC_HOSTNAME_ADMIN: Keycloak 26 rendert alle Admin-Console-URLs (Cookies, OIDC-Redirects, interne Links) gegenauth-admin.targetshot.app. Der/admin/*-Endpoint ANTWORTET zwar weiterhin auch aufauth.targetshot.appmit HTTP 200 (KC erzwingt kein Host-Allowlisting per Pfad), aber Login funktioniert praktisch nur über den Admin-Hostname weil Cookies + Redirects dahin gehen. Im Bicep gesetzt + per App Setting live. - [ ] 2FA auf den echten Admin-User: Beim ersten Login als
admineinen User mit deiner Mail-Adresse anlegen, der Rolleadmin(Realm-Role im master-Realm) bekommt. Required Action „Configure OTP" für den User aktivieren, ausloggen, neu einloggen → TOTP-App (Authy/1Password) registrieren. Erst dann den Bootstrap-adminlöschen. - [ ] Brute-Force-Schutz: master-Realm → Realm settings →
Security defenses → Brute force detection →
Enabled(Standard: 30 Fails / 15 min Lockout). Reicht gegen Wörterbuch-Attacken.
Gegen einen gezielten Angreifer, der den Admin-Hostnamen kennt UND das 2FA-Secret hat, hilft das nicht — dafür bräuchte es Front Door davor mit Path-Routing + WAF (€€/Monat). Aktuell zu teuer für den Threat-Level.
Realm-Migration¶
- [ ] Bestehende Realms auf dem on-prem Keycloak exportieren
(
kc.sh export --dir /tmp/export --users realm_file --realm <realm>). - [ ] Export-Files ins Image kopieren oder via
KC_IMPORT_REALM=/opt/keycloak/data/importbeim ersten Start einspielen. - [ ] Separates Runbook für User-Migration schreiben (Postgres-Dump
- Restore) — nur wenn Realm-Level-User-Export nicht ausreicht.
HAProxy-Anpassung (bestehender on-prem Edge)¶
- [ ] In
infrastructure/haproxy/haproxy.cfgkc_backendstilllegen oder auf Staging-IP umbiegen. Prod-Auth geht komplett über die Azure-Custom-Domains, nicht mehr über HAProxy. - [ ] Host-ACLs für
auth.targetshot.app+auth-admin.targetshot.appaus dercf_protected_host-Liste entfernen (beide sind bewusst nicht hinter CF-Proxy — DNS-only).
Rollback-Plan¶
- [ ] TTL der neuen CNAMEs vorab auf 5 Minuten setzen, damit ein Rollback via DNS-Switch auf das on-prem Keycloak innerhalb einer Viertelstunde durchschlägt.
- [ ] Staging-Keycloak bleibt aktiv — dient als Fallback-IdP mit identischem Realm-Export, falls die Azure-Instanz sich nicht bootet.
Ingest: Prod-Migration auf Azure Functions + Postgres Flexible Server¶
Design-Dokument: Ingest auf Azure Functions + Postgres Flex.
Staging bleibt auf dem bestehenden Compose-Ingest (ts-admin-portal);
der Cutover betrifft nur den produktiven Schreibpfad.
**Status 2026-09-01 (nachgeführt; live seit 2026-04-30, Sprint 19
110 / Phase 5):** Der Prod-Ingest läuft produktiv auf Azure¶
Functions — Function-App
targetshot-ingest-prd01(RGtargetshot-kc, geteilter P0v3-Plankeycloak-prod-plan) mit vier EventHub-Trigger-Functions, Consumer-Grouptargetshot-prod-ingest, DBtargetshotauf dem geteilten Flex Servertargetshot-kc-pg-prd01-we. Metriken gesund (Stichprobe 2026-08-29: 16 620 Treffer-Events ohne Lag). Abweichungen ggü. dem Plan unten als*Δ*markiert (perazverifiziert 2026-09-01). Echter Rest: Deploy-Workflow, Managed-Identity-Auth zur Flex-DB, Lag-Dashboard-Frage, Phase 6 Multi-Host (s. u.).
Infrastruktur¶
- [x] Δ Eigene Resource Group
targetshot-prod-ingestentfällt — die Function-App läuft in der bestehenden RGtargetshot-kcneben Keycloak (ein gemeinsamer Prod-Stack statt zweiter Verwaltungsfläche). - [x] Δ Kein dedizierter Flex Server
Standard_B2s— der Ingest nutzt den bestehenden Keycloak-Flex-Servertargetshot-kc-pg-prd01-we(Standard_B1ms,westeurope, 7-Tage-PITR, Public Access + „Allow Azure services", s. Keycloak-Abschnitt) mit eigener Datenbanktargetshot. - [ ] Entra-Admin auf dem Flex Server setzen
(
az postgres flexible-server ad-admin create), damit Managed Identity-Auth möglich bleibt. Stand 2026-09-01: offen — hängt am Managed-Identity-Umstieg (s. Function-App-Identity unten). - [x] Δ Storage Account heißt
tsingestprd01(Standard LRS, RGtargetshot-kc) statttargetshotprodfunc<rand>. - [x] Δ Function App heißt
targetshot-ingest-prd01und läuft auf dem geteilten, bereits vorhandenen P0v3-Plan (keycloak-prod-plan) statt Linux Consumption — kein Cold-Start, keine Zusatzkosten für einen zweiten Plan. Runtime Node 20 statt geplantem Node 22. System-Assigned Managed Identity ist aktiviert. - [ ] Function-App-Identity als Postgres-Role
(
GRANT CONNECT ... ; GRANT SELECT/INSERT/UPDATE/DELETE ... ;) in der Flex-DB anlegen. Stand 2026-09-01: offen — die Function connectet per Passwort-User (App-SettingsTARGETSHOT_DATABASE_*); MI-Auth zusammen mit dem Entra-Admin-Punkt oben nachziehen.
Event Hubs¶
- [x] Consumer-Group
targetshot-prod-ingestauf allen 4prod.sds.*-Hubs peraz eventhubs eventhub consumer-group create ...anlegen bevor die Function erstmalig startet. Sonst stiller Retry wie beim Staging-Cutover 2026-04-24. (Auf allen 4 Hubs vorhanden, perazverifiziert 2026-09-01.) - [x] Δ SAS-Policy
prod-listen(Listen only) existiert auf Namespace-Ebene (targetshot-eh), nicht je Hub; das Function-App-Setting heißtEVENTHUB_LISTEN_CSstattEVENTHUBS_CONNECTION(Code-Default inpackages/functions-ingest/src/index.ts).
Schema¶
- [x]
npx prisma migrate deployeinmalig gegen den frischen Flex Server ausführen (von einer Dev-IP, die in der Firewall steht). (Erledigt zum Go-Live 2026-04-30 — Ziel-DB isttargetshotauf dem geteilten Flex Server, Schema nimmt seither Events an.) - [x] Prisma Client ins Function-App-Deploy bundlen. (Das
Deploy-Skript
packages/functions-ingest/scripts/package-for-deploy.shkopiertsrc/generated/prisma-client/samt Engine-Binaries in dieapp.zip.) - [x] Keine Daten-Migration aus Staging — Prod startet leer und wird über Debezium-Snapshots der ersten produktiv ausgerollten Clubs befüllt. (So passiert: erster Verein per Debezium-Snapshot, seither laufender Event-Strom.)
Function-App-Code¶
- [x] Δ Code liegt als Workspace-Package
packages/functions-ingest/(nichttargetshot/ingest-functions/); die vier Trigger heißenIngestSchuetze,IngestTreffer,IngestScheiben,IngestSerien. - [x] Δ Statt Portierung: die Upsert-/Handler-Logik wurde ins
geteilte Package
packages/ingest-coreextrahiert —backend/src/ingest/(Staging-Pfad) und die Function-App rufen denselbenprocessMessage-Code auf, Prod- und Staging-Ingest bleiben damit deckungsgleich. - [x] Δ
host.jsonist überextensions.eventHubsgetuned (maxEventBatchSize: 64,prefetchCount: 256,batchCheckpointFrequency: 1) — die geplanten KeysbatchSize/maxConcurrencyheißen im v4-Programmiermodell anders. - [ ] GitHub Action
deploy-prod-ingest-functions.ymlmitAzure/functions-action@v1+ OIDC-Login einrichten. Stand 2026-09-01: offen — Deploy läuft manuell als Zip (packages/functions-ingest/scripts/package-for-deploy.sh→az functionapp deployment source config-zip).
Verifikation nach Cutover¶
- [x] Δ Test-Message-Ritual ist durch den Live-Betrieb überholt:
seit 2026-04-30 fließen echte
prod.sds.*-Events durch die Function in die Flex-DB — der Ende-zu-Ende-Schreibpfad ist damit laufend belegt. - [x]
IncomingMessages+OutgoingMessagesauf EH für die Prod-Topics monitoren; Function-App Logs aufUnauthorized/InvalidCredentialschecken. (Metrik-Stichprobe 2026-08-29: 16 620 Treffer-Events ohne Lag, keine Auth-Fehler.) - [ ] Ingest-Lag-Dashboard (kommt mit App-Insights) zeigt < 30 s. Stand 2026-09-01: offen als Grundsatzfrage — die Lag-Sicht lebt im admin-portal (Cloud-Ingest-Observability), nicht in App Insights. Entscheiden, ob ein App-Insights-Dashboard zusätzlich überhaupt gebraucht wird.
Rollback¶
- [x] Function App per Portal auf „Stopped" — EH puffert bis 7 Tage,
nichts geht verloren. (Mechanismus steht: EH-Retention 168 h, per
azverifiziert 2026-09-01.) - [x] Flex Server per PITR auf T-5 min zurück (bei DB-Korruption). (7-Tage-PITR am geteilten Flex Server konfiguriert, s. Keycloak-Abschnitt.)
- [x] Δ „Vor echtem Prod-Traffic: System ist beliebig oft wegwerfbar" gilt nicht mehr — seit 2026-04-30 liegen echte Prod-Daten in der DB. Rollback heißt jetzt Function stoppen + PITR, Wegwerfen ist keine Option mehr.
Offen nach Go-Live (Stand 2026-09-01)¶
- [ ] Phase 6 Multi-Host: Rollout eines zweiten Vereins-Hosts — bewusst geblockt, bis der zweite Verein konkret ansteht.