Renames stale ActiveNodeHealthCheck references to OldestNodeActiveHealthCheck (Task 7 rename) across the requirements + components doc sets, corrects leader→oldest-member wording and the embedded IsActiveNode snippet, and adds an explicit 'production TLS profile not yet implemented' roadmap note. deploy/wonder-app-vd03 NodeName overlay edits deferred: that production deploy artifact is not tracked in this repo (same as Tasks 16 & 20).
7.1 KiB
Component: Traefik Proxy
Purpose
The Traefik Proxy is a reverse proxy and load balancer that sits in front of the central cluster's two web servers. It provides a single stable URL for the CLI, browser, and external API consumers, automatically routing traffic to the active central node. When the active node fails over, Traefik detects the change via health checks and redirects traffic to the new active node without manual intervention.
Location
Runs as a Docker container (scadabridge-traefik) in the cluster compose stack (docker/docker-compose.yml). Not part of the application codebase — it is a third-party infrastructure component with static configuration files.
docker/traefik/
Responsibilities
- Route all HTTP traffic (Central UI, Management API, Inbound API, health endpoints) to the active central node.
- Health-check both central nodes via
/health/activeto determine which is the active (oldest Up member / singleton host) node. - Automatically fail over to the standby node when the active node goes down.
- Provide a dashboard for monitoring routing state and backend health.
How It Works
Active Node Detection
Traefik polls /health/active on both central nodes every 5 seconds. This endpoint returns:
- HTTP 200 on the active node (the oldest Up cluster member — the node that hosts the cluster singletons).
- HTTP 503 on the standby node (or if the node is unreachable, or if its database is not reachable).
Only the node returning 200 receives traffic. The health check is implemented by OldestNodeActiveHealthCheck in the Host project, which reports Healthy only when ClusterActivityEvaluator.SelfIsOldest(cluster) is true — the oldest-member definition of "active", not the cluster leader (leader and oldest can differ transiently during membership churn; singletons follow the oldest member, so routing must too). /health/active is additionally tagged with the database readiness check, so a node whose database is unreachable sheds traffic even while it is the oldest member.
Failover Sequence
- Active node fails (crash, network partition, or graceful shutdown).
- Akka.NET cluster detects the failure (~10s heartbeat timeout).
- Split-brain resolver acts after stable-after period (~15s).
- Surviving node becomes the oldest Up member and the singletons migrate to it.
OldestNodeActiveHealthCheckon the surviving node starts returning 200 (its database check must also pass).- Traefik's next health poll (within 5s) detects the change.
- Traffic routes to the new active node.
Total failover time: ~25–30s (Akka failover ~25s + Traefik poll interval up to 5s).
SignalR / Blazor Server Considerations
Blazor Server uses persistent SignalR connections (WebSocket circuits). During failover:
- Active SignalR circuits on the failed node are lost.
- The browser's SignalR reconnection logic attempts to reconnect.
- Traefik routes the reconnection to the new active node.
- The user's session survives because authentication uses cookie-embedded JWT with shared Data Protection keys across both central nodes.
- The user may see a brief "Reconnecting..." overlay before the circuit re-establishes.
Configuration
Static Config (docker/traefik/traefik.yml)
entryPoints:
web:
address: ":80"
api:
dashboard: true
insecure: true
providers:
file:
filename: /etc/traefik/dynamic.yml
- Entrypoint
web: Listens on port 80 (mapped to host port 9000). - Dashboard: Enabled in insecure mode (no auth) for development. Accessible at
http://localhost:8180. - File provider: Loads routing rules from a static YAML file (no Docker socket required).
Dynamic Config (docker/traefik/dynamic.yml)
http:
routers:
central:
rule: "PathPrefix(`/`)"
service: central
entryPoints:
- web
services:
central:
loadBalancer:
healthCheck:
path: /health/active
interval: 5s
timeout: 3s
servers:
- url: "http://scadabridge-central-a:5000"
- url: "http://scadabridge-central-b:5000"
- Router
central: Catches all requests and forwards to thecentralservice. - Service
central: Load balancer with two backends (both central nodes) and a health check on/health/active. - Health check interval: 5 seconds. A server failing the health check is removed from the pool within one interval.
Ports
| Host Port | Container Port | Purpose |
|---|---|---|
| 9000 | 80 | Load-balanced entrypoint (Central UI, Management API, Inbound API) |
| 8180 | 8080 | Traefik dashboard |
Health Endpoints
The central nodes expose three health endpoints:
| Endpoint | Purpose | Who Uses It |
|---|---|---|
/health/ready |
Readiness gate — 200 when database + akka-cluster + required-singletons are healthy | Kubernetes probes, monitoring |
/health/active |
Active node — 200 only on the oldest Up member (singleton host) whose database is also reachable | Traefik (routing decisions) |
Dependencies
- Central cluster nodes: The two backends (
scadabridge-central-a,scadabridge-central-b) on thescadabridge-netDocker network. - OldestNodeActiveHealthCheck: Health check implementation in
src/ZB.MOM.WW.ScadaBridge.Host/Health/OldestNodeActiveHealthCheck.csthat reports the oldest-Up-member (singleton-host) status viaClusterActivityEvaluator.SelfIsOldest;/health/activealso carries the database readiness check. - Docker network: All containers must be on the shared
scadabridge-netbridge network.
Interactions
- CLI: Connects to
http://localhost:9000/management— routed by Traefik to the active node. - Browser (Central UI): Connects to
http://localhost:9000— Blazor Server + SignalR routed to the active node. - Inbound API consumers: Connect to
http://localhost:9000/api/{methodName}— routed to the active node. - Cluster Infrastructure: The
OldestNodeActiveHealthCheckrelies on Akka.NET cluster membership state to determine the oldest Up member (the singleton host).
Production Considerations
Production TLS profile: not yet implemented. Traefik terminates plain HTTP, Akka remoting is unencrypted TCP, and site gRPC is
http://. This is acceptable for the lab topology only; a production deployment requires TLS at all three layers (Traefik HTTPS entrypoint, Akka remoting TLS, gRPC overhttps://) and a secured Traefik dashboard. Tracked as an open roadmap item (arch review 01, underdeveloped area 3).
The current configuration is for development/testing. In production:
- TLS termination: Add HTTPS entrypoint with certificates (Let's Encrypt via Traefik's ACME provider, or static certs).
- Dashboard auth: Disable
insecure: trueand configure authentication on the dashboard. - WebSocket support: Traefik supports WebSocket proxying natively — no additional config needed for SignalR.
- Sticky sessions: Not required. The Management API is stateless (Basic Auth per request). Blazor Server circuits are bound to a specific node via SignalR, but reconnection handles failover transparently.