Skip to content

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.yml duplizieren als cleanup-orphan-photos-prod.yml.
  • [ ] Im neuen Workflow runs-on-Label auf den Prod-Runner anpassen (z. B. [self-hosted, linux, x64, prod]).
  • [ ] DEPLOY_DIR auf secrets.PROD_DOCKER_COMPOSE_DIR umbiegen.
  • [ ] Prüfen, dass im Prod-Deploy (deploy-prod.yml o. Ä.) die Env-Vars AZURE_STORAGE_CONNECTION_STRING + AZURE_STORAGE_PHOTOS_CONTAINER aus secrets.PROD_AZURE_STORAGE_CONNECTION_STRING und vars.PROD_AZURE_STORAGE_PHOTOS_CONTAINER=photos-prod injiziert werden (analog zum Block in deploy-staging.yml, der am 2026-04-23 angelegt wurde).
  • [ ] Einmaligen Dry-Run über gh workflow run cleanup-orphan-photos-prod.yml ausfü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-staging vs. photos-prod). Der Cleanup-Workflow diff't immer nur gegen die DB, die sein DATABASE_URL erreicht — solange PROD_DATABASE_URL + photos-prod zusammen 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/master antwortet 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 Subnet app (10.20.1.0/24), delegated zu Microsoft.Web/serverFarms.
  • [x] Δ db-Subnet entfällt — Postgres läuft mit **Public Endpoint
  • Firewall-Regel AllowAllAzureServicesAndResourcesWithinAzureIps** statt private VNet-Integration. Grund: LocationIsOfferRestricted für Flex-Server in Germany West Central in unserer Subscription. Postgres läuft daher in westeurope (ü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_B1ms in westeurope, Server-Name targetshot-kc-pg-prd01-we, Admin-User pgadmin, 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-prd01 für DB-Passwort, Web App holt KC_DB_PASSWORD via @Microsoft.KeyVault(...) Reference (Status per az rest /configreferences: Resolved).

Datenbank-Bootstrap

  • [x] keycloak-Datenbank wird durch Bicep (Microsoft.DBforPostgreSQL/flexibleServers/databases) angelegt. Schema-Migration übernimmt Liquibase beim ersten Keycloak-Start; manuelles CREATE USER/GRANT entfällt, weil Keycloak mit dem Server-Admin (pgadmin) connected.
  • [x] Passwort in Key Vault unter kc-db-password (vom deploy.sh beim 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 build im Builder-Stage, start --optimized als CMD).
  • [x] Workflow infrastructure/.github/workflows/build-keycloak-image.yml baut + pusht bei Push auf keycloak/** nach targetshotkcprd01.azurecr.io/keycloak:<sha>. Pre-conds: Federated Credential für repo:targetshot-app/infrastructure: ref:refs/heads/main + AcrPush-Rolle auf der ACR sind angelegt.
  • [ ] Optionaler deploy-keycloak-prod.yml (rollt neue Tags automatisch via az webapp config container set aus). Aktuell manuell — passt für die niedrige Release-Frequenz.

Domains

  • [x] Cloudflare DNS: CNAME authkeycloak-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.app gebunden (CNAME-Verifikation reichte, kein TXT nötig). Managed Certificate via az 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_HOSTNAME auf https://auth.targetshot.app umgestellt (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 (pathPattern nicht 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) gegen auth-admin.targetshot.app. Der /admin/*-Endpoint ANTWORTET zwar weiterhin auch auf auth.targetshot.app mit 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 admin einen User mit deiner Mail-Adresse anlegen, der Rolle admin (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-admin lö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/import beim 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.cfg kc_backend stilllegen 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.app aus der cf_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-ingest in 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-ingest auf allen 4 prod.sds.*-Hubs per az 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-Setting EVENTHUBS_CONNECTION hinterlegen.

Schema

  • [ ] npx prisma migrate deploy einmalig 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.json mit batchSize und maxConcurrency passend zum erwarteten Durchsatz konfigurieren (Default reicht bis ~1 k evt/s).
  • [ ] GitHub Action deploy-prod-ingest-functions.yml mit Azure/functions-action@v1 + OIDC-Login einrichten.

Verifikation nach Cutover

  • [ ] Test-Message via Staging-ts-connect mit temporär auf prod.sds-Prefix umgebogenem Env an prod.sds.schuetze schicken, Row in Flex DB prüfen, Event entfernen.
  • [ ] IncomingMessages + OutgoingMessages auf EH für die Prod-Topics monitoren; Function-App Logs auf Unauthorized/InvalidCredentials checken.
  • [ ] 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.