Ingest auf Azure Functions + Postgres Flex — Prod-Deployment-Design¶
Design-Dokument für den produktiven Ingest-Pfad nach dem Event-Hubs-Cutover
(siehe Confluent → Event Hubs Cutover). Staging bleibt unverändert —
die Compose-basierte Ingest-Worker-Flotte auf ts-admin-portal ist dort
gut genug. Prod zieht auf Azure Functions (Consumption) → Azure Database
for PostgreSQL Flexible Server (Public Endpoint + Firewall) um.
Zielbild¶
- Ingest als Function, nicht als Compose-Service: ein Azure Function App pro Umgebung, Event-Hubs-Trigger, Node 22 Runtime. Kein Host-Patching, kein Docker-Stack, kein Restart-Handling.
- Managed Postgres: Flexible Server mit Public-Endpoint + Firewall-Rules als Start — VNet-Integration lassen wir bewusst weg, bis wir den operativen Overhead rechtfertigen können.
- Consumption Plan: pay-per-execution, skaliert automatisch mit dem Event-Hubs-Durchsatz. Cold-Start (~1–2 s) akzeptabel, da CDC-Durchlauf eventual-consistent ist — kein Real-Time-UI hängt direkt am Schreibpfad.
- Prisma bleibt: das bestehende Schema und alle Upsert-Queries wandern
unverändert mit; nur
DATABASE_URLzeigt auf Flex Server.
Komponenten¶
| Ressource | SKU / Config | Zweck |
|---|---|---|
| Resource Group | targetshot-prod-ingest in Germany West Central |
Kapselung |
| Storage Account | Standard LRS (targetshotprodfunc<rand>) |
Function-Runtime-State (Pflicht) |
| Function App | Linux Consumption, Node 22 | Ingest-Consumer |
| Postgres Flex | Standard_B2s (2 vCPU, 4 GB), 64 GB SSD, Burstable |
App-Datenbank |
| — Backup | 7 Tage PITR | Wiederherstellung |
| — Auth | Entra ID + Managed Identity (empfohlen) oder Passwort | Function-App-Login |
| Event Hubs | bestehender Namespace targetshot-eh |
Quelle (Topics prod.sds.*) |
| — SAS Policy | prod-listen (Listen only) |
Function-App-Connection |
Bewusst nicht drin: VNet, Private Endpoint, NAT Gateway, Application Insights (kommt erst wenn Observability-Bedarf steigt), Azure Front Door.
Netzwerk & Security¶
- Postgres Flex Public Access, Firewall-Rules:
- „Allow Azure services and resources to access this server" = On (Consumption-Plan Functions haben rotierende Outbound-IPs, nur so erreichbar).
- Eigene Dev-IP für Admin-Zugriff + Migration-Runs.
- Auth-Pfad:
- Primär: Entra-Managed-Identity +
DefaultAzureCredentialin der Function, Postgres-Role peraz postgres flexible-server ad-admin create. Kein Passwort in App-Settings, kein Key Vault nötig. - Fallback: klassisches Passwort in App-Settings, wenn Managed Identity im ersten Cutover hakt. Passwort dann in Key Vault, Function holt via Reference.
- SSL:
sslmode=requireinDATABASE_URL; Flex Server erzwingt TLS.
Function-App-Struktur¶
Neues Repo-Subfolder targetshot/ingest-functions/ (oder separates Repo,
je nach CI-Gewohnheit). Ein einziger Event-Hubs-Trigger, der alle vier
Topics per Pattern abgreift:
ingest-functions/
├── host.json # Runtime-Config (batchSize, maxConcurrency)
├── package.json # deps: @azure/functions, kafkajs — oder direkt
│ # @azure/event-hubs + @azure/identity
├── src/
│ ├── functions/
│ │ └── sdsIngest.ts # app.eventHub("sds", { ... })
│ ├── handlers/ # entity-spezifische Upsert-Logik
│ │ ├── schuetze.ts
│ │ ├── treffer.ts
│ │ ├── scheiben.ts
│ │ └── serien.ts
│ ├── db/
│ │ └── prismaClient.ts # einmaliger Singleton pro Instanz
│ └── topicContract.ts # aus backend/src/ingest/ migriert, 1:1
└── prisma/
└── schema.prisma # symlink oder kopie des backend-Schemas
Trigger pro Event Hub (nicht ein Pattern!). Azure Functions v4 unterstützt einen Event-Hub-Trigger pro Function, aber man kann eine Function pro Hub anlegen — alle routen in die gleiche Upsert-Pipeline, die anhand des Topic-Namens den Handler auswählt (dieselbe Logik wie heute im Backend-Ingest).
import { app, InvocationContext } from "@azure/functions";
app.eventHub("schuetzeIngest", {
connection: "EVENTHUBS_CONNECTION",
eventHubName: "prod.sds.schuetze",
consumerGroup: "targetshot-prod-ingest",
cardinality: "many",
handler: (messages, ctx) => handleBatch("prod.sds.schuetze", messages, ctx),
});
// analog treffer, scheiben, serien
cardinality: "many" gibt uns Batches statt Einzelevents → Batch-Upsert
in Postgres + ein Offset-Commit pro Batch.
Consumer-Groups auf Event Hubs¶
Wichtig, nicht vergessen: Event Hubs Standard legt Consumer-Groups
nicht auto-an, wenn der Kafka-Client sich mit einem neuen groupId
verbindet. Vor dem Cutover per CLI erstellen:
for eh in prod.sds.schuetze prod.sds.treffer prod.sds.scheiben prod.sds.serien; do
az eventhubs eventhub consumer-group create \
--resource-group targetshot-eventhubs \
--namespace-name targetshot-eh \
--eventhub-name "$eh" \
--name targetshot-prod-ingest
done
Genau dieser Fehler hat uns beim Staging-Cutover blockiert — Ingest
hing im stillen Retry, weil die targetshot-backend-ingest-Group nicht
existierte und EH den Subscribe-Call stumm abgelehnt hat.
Schema-Migration¶
- Vorbereitung auf dem Dev-Rechner:
DATABASE_URL="postgres://pgadmin:<pwd>@targetshot-prod-pg.postgres.database.azure.com:5432/targetshot?sslmode=require" npx prisma migrate deploy --schema backend/prisma/schema.prisma - Tabellen existieren leer. Dev-IP muss zu dem Zeitpunkt in der Postgres-Firewall stehen (Azure Portal → Networking → Add client IP).
- Prisma Client wird mit demselben Schema-Hash generiert und in die Function-App gebaut.
Data-Migration (bestehende Staging-Daten)¶
Entscheidung: Prod startet leer, nicht mit Staging-Dump. Grund:
der Staging-DB-Inhalt ist Test-CDC-Data, keine echten Vereins-Einträge.
Bei echten Clubs übernimmt ts-connect den initialen Backfill aus der
jeweiligen Vereins-DB per Debezium-Snapshot, sobald der erste Club mit
Prod-Connector-Config ausgerollt wird.
Falls später doch ein Prod-Staging-Dump nötig wird:
pg_dump vom Staging-Server → pg_restore gegen Flex — Schema-Kompatibilität
ist durch Prisma garantiert, solange beide auf der gleichen Migration
stehen.
Deployment¶
GitHub Actions Workflow deploy-prod-ingest-functions.yml:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci --prefix ingest-functions
- run: npx prisma generate --schema backend/prisma/schema.prisma
- run: npm run build --prefix ingest-functions
- uses: azure/login@v2
with: { creds: ${{ secrets.AZURE_CREDENTIALS }} }
- uses: Azure/functions-action@v1
with:
app-name: targetshot-prod-ingest
package: ingest-functions
App-Settings werden einmalig per az functionapp config appsettings set
gesetzt:
EVENTHUBS_CONNECTION=Endpoint=sb://targetshot-eh.servicebus.windows.net/;SharedAccessKeyName=prod-listen;SharedAccessKey=...;EntityPath=prod.sds.schuetze
DATABASE_URL=postgres://...
FUNCTIONS_WORKER_RUNTIME=node
Bei Managed-Identity-Auth entfällt das Passwort in DATABASE_URL;
stattdessen baut die Function den Connection-String zur Laufzeit mit
einem Entra-Access-Token als Passwort.
Kosten (Germany West Central, ca., pay-as-you-go)¶
| Posten | Monatlich |
|---|---|
| Function App Consumption (erste 1 M exec gratis, dann €0,17/M) | ~€0–5 |
| Storage Account (Function Runtime) | ~€1 |
Postgres Flex B2s + 64 GB + 7 d Backup |
~€45 |
| Event Hubs (unverändert, bereits gebucht) | €0 neu |
| Summe | ~€50 / Monat |
Bei steigendem Durchsatz oder strengerer Latenz kann der Function-Plan
später auf Premium EP1 (~€140/mo, kein Cold-Start, VNet möglich)
hochgezogen werden, ohne dass der Code sich ändert.
Cutover-Schritte (High-Level)¶
- Flex Server + Function-App anlegen, Firewall für Dev-IP öffnen.
prisma migrate deploygegen den frischen Flex Server.- EH-Consumer-Groups
targetshot-prod-ingestauf allen 4prod.sds.*Hubs erstellen. - Function-App deployen, App-Settings setzen, Test-Message auf
prod.sds.schuetzeproduzieren (via Staging-ts-connect mit umgebogenem Prefix für 1 Request), Row in Flex DB verifizieren. - Prod-Frontend + Prod-Main-App-Deploy nachziehen (eigener Cutover,
separat — siehe
prod-cutover-checklist.md). - Erster echter Verein rollt aus:
ts-connectmitprod-sendSAS-Policy + Prefixprod.sds; Debezium-Snapshot füllt die frische DB.
Rollback-Plan¶
- Funktion App auf
Stoppedsetzen → Ingest pausiert, EH puffert bis 7 Tage Retention. - Falls DB kaputt: Flex Server per PITR auf T-5-Minuten zurück (7 Tage Historie).
- Kompletter Rollback: Prod-Traffic gibt's zum Cutover-Zeitpunkt noch nicht — wir können das System so oft neu aufbauen, wie nötig.
Was dieses Dokument noch nicht enthält¶
- Bicep- oder Terraform-Dateien — kommt als nächster Schritt.
- Application-Insights-Setup (Logs + Traces).
- Alerting auf Ingest-Lag / Function-Errors.
- Multi-Region-Failover (Flex Server kann Replicas, Function-App kann Zonenredundanz — beides erst relevant wenn Prod-Traffic messbar ist).