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¶
- [ ] Datei
.github/workflows/cleanup-orphan-photos-staging.ymlduplizieren alscleanup-orphan-photos-prod.yml. - [ ] Im neuen Workflow
runs-on-Label auf den Prod-Runner anpassen (z. B.[self-hosted, linux, x64, prod]). - [ ]
DEPLOY_DIRaufsecrets.PROD_DOCKER_COMPOSE_DIRumbiegen. - [ ] Prüfen, dass im Prod-Deploy (
deploy-prod.ymlo. Ä.) die Env-VarsAZURE_STORAGE_CONNECTION_STRING+AZURE_STORAGE_PHOTOS_CONTAINERaussecrets.PROD_AZURE_STORAGE_CONNECTION_STRINGundvars.PROD_AZURE_STORAGE_PHOTOS_CONTAINER=photos-prodinjiziert werden (analog zum Block indeploy-staging.yml, der am 2026-04-23 angelegt wurde). - [ ] Einmaligen Dry-Run über
gh workflow run cleanup-orphan-photos-prod.ymlausführen, Output prüfen — im Prod-Rollout gibt es zum Zeitpunkt T=0 noch keine Orphans, der erste echte Durchlauf lohnt sich erst nachdem ein paar Foto-Updates passiert sind. - [ ] Keine Shared-Container-Gefahr: Staging und Prod teilen zwar das
Storage-Account, aber jede Umgebung hat einen eigenen Container
(
photos-stagingvs.photos-prod). Der Cleanup-Workflow diff't immer nur gegen die DB, die seinDATABASE_URLerreicht — solangePROD_DATABASE_URL+photos-prodzusammen laufen, kann er nicht versehentlich Staging-Blobs löschen.
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.
Infrastruktur¶
- [ ] Resource Group
targetshot-prod-ingestin Germany West Central anlegen. - [ ] Azure Database for PostgreSQL Flexible Server
Standard_B2s(2 vCPU, 4 GB), 64 GB SSD, 7 Tage PITR, Public Access, Firewall „Allow Azure services" = On + eigene Dev-IP. - [ ] Entra-Admin auf dem Flex Server setzen
(
az postgres flexible-server ad-admin create), damit Managed Identity-Auth möglich bleibt. - [ ] Storage Account
targetshotprodfunc<rand>Standard LRS (Function-Runtime-State). - [ ] Azure Function App
targetshot-prod-ingest, Linux Consumption, Node 22, System-Assigned Managed Identity aktivieren. - [ ] Function-App-Identity als Postgres-Role
(
GRANT CONNECT ... ; GRANT SELECT/INSERT/UPDATE/DELETE ... ;) in der Flex-DB anlegen.
Event Hubs¶
- [ ] 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. - [ ] SAS-Policy
prod-listen(Listen only) als Function-App-SettingEVENTHUBS_CONNECTIONhinterlegen.
Schema¶
- [ ]
npx prisma migrate deployeinmalig gegen den frischen Flex Server ausführen (von einer Dev-IP, die in der Firewall steht). - [ ] Prisma Client ins Function-App-Deploy bundlen.
- [ ] Keine Daten-Migration aus Staging — Prod startet leer und wird über Debezium-Snapshots der ersten produktiv ausgerollten Clubs befüllt.
Function-App-Code¶
- [ ] Neuen Ordner
targetshot/ingest-functions/anlegen mit je einem Event-Hubs-Trigger pro Hub (schuetzeIngest,trefferIngest,scheibenIngest,serienIngest). - [ ] Upsert-Handler aus
backend/src/ingest/portieren (Topic-Contract - Entity-Handler lassen sich 1:1 übernehmen).
- [ ]
host.jsonmitbatchSizeundmaxConcurrencypassend zum erwarteten Durchsatz konfigurieren (Default reicht bis ~1 k evt/s). - [ ] GitHub Action
deploy-prod-ingest-functions.ymlmitAzure/functions-action@v1+ OIDC-Login einrichten.
Verifikation nach Cutover¶
- [ ] Test-Message via Staging-
ts-connectmit temporär aufprod.sds-Prefix umgebogenem Env anprod.sds.schuetzeschicken, Row in Flex DB prüfen, Event entfernen. - [ ]
IncomingMessages+OutgoingMessagesauf EH für die Prod-Topics monitoren; Function-App Logs aufUnauthorized/InvalidCredentialschecken. - [ ] Ingest-Lag-Dashboard (kommt mit App-Insights) zeigt < 30 s.
Rollback¶
- [ ] Function App per Portal auf „Stopped" — EH puffert bis 7 Tage, nichts geht verloren.
- [ ] Flex Server per PITR auf T-5 min zurück (bei DB-Korruption).
- [ ] Vor echtem Prod-Traffic: System ist beliebig oft wegwerfbar.