From 6e7d88db49cf260abf34c32b294f874ed51397b0 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 11 May 2026 19:02:58 -0500 Subject: [PATCH] feat(fc-redis): add SignalR backplane for cross-product event bus (Q-SO-1 Phase A) Per Q-SO-1 operator resolution 2026-05-11 PM, Redis SignalR backplane lands in Phase A (was Phase C deferral). Treats Redis as a managed FC infrastructure component, not a deferred scaling escalation. Lands the minimal Phase A surface: - Namespace fc-redis - Single Redis 7-alpine pod with 1Gi Longhorn RWO PVC - ConfigMap with AOF persistence (everysec), 256Mi maxmemory, allkeys-lru - ClusterIP Service `redis.fc-redis.svc.cluster.local:6379` (in-cluster only) - No AUTH Phase A (Phase B add via 1Password Connect rotation) - No IngressRoute (backplane is server-to-server) Consumers (Phase A IMPL across FC services) add: services.AddSignalR().AddStackExchangeRedis( "redis.fc-redis.svc.cluster.local:6379", opts => opts.Configuration.ChannelPrefix = StackExchange.Redis.RedisChannel.Literal("fc-opsconsole")); Phase B/C follow-ons (not in this commit): Sentinel for HA, AUTH password from 1Password, redis_exporter sidecar for Prometheus, network policies. See FlowerCore.Notes/docs/signage/operations-console-phase-2-design.md section 3.5 (rewritten) and decisions-waiting.html Q-SO-1 (RESOLVED). Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/fc-redis/fc-redis.yaml | 171 ++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 apps/fc-redis/fc-redis.yaml diff --git a/apps/fc-redis/fc-redis.yaml b/apps/fc-redis/fc-redis.yaml new file mode 100644 index 0000000..aaa2446 --- /dev/null +++ b/apps/fc-redis/fc-redis.yaml @@ -0,0 +1,171 @@ +# fc-redis — SignalR backplane for cross-product event bus +# +# Lands per Q-SO-1 resolution (2026-05-11 PM): SignalR backplane in Phase A, +# not Phase C as originally drafted. Operator directive: "Redis can be +# deployed just fine as it's another FlowerCore technology we'll want to +# manage." +# +# Phase A scope (this file): +# - Single Redis 7.x Alpine pod +# - 1Gi Longhorn RWO PVC for AOF persistence +# - ClusterIP Service at `redis.fc-redis.svc.cluster.local:6379` +# - No AUTH (in-cluster only; not exposed externally) +# - No IngressRoute (backplane is server-to-server only) +# +# Consumers (Phase A IMPL across FC services): +# - FlowerCore.Signage.Web (OpsConsoleHub) +# - FlowerCore.Scoreboard.Web (ScoreboardHub) +# - FlowerCore.SignalControl.Web +# - FlowerCore.DMS.Web +# - Any other product joining the cross-product event bus +# +# Each consumer adds: +# services.AddSignalR() +# .AddStackExchangeRedis( +# "redis.fc-redis.svc.cluster.local:6379", +# opts => opts.Configuration.ChannelPrefix = +# StackExchange.Redis.RedisChannel.Literal("fc-opsconsole")); +# +# Phase B / C follow-ons (out of scope here): +# - Redis Sentinel for HA (3-node) +# - AUTH password from 1Password Connect (rotate via /rotate-password) +# - redis_exporter sidecar for Prometheus scrape +# - Network policies restricting which namespaces can dial 6379 +# +# Design: docs/signage/operations-console-phase-2-design.md §3.5 +# Decision: Q-SO-1 (RESOLVED 2026-05-11 PM) +# Memory: feedback_blooming_ui_pattern_no_iframes +--- +apiVersion: v1 +kind: Namespace +metadata: + name: fc-redis + labels: + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: fc-redis-data + namespace: fc-redis +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: fc-redis-config + namespace: fc-redis +data: + redis.conf: | + # Phase A — minimal config; no AUTH, no replication. + bind 0.0.0.0 + protected-mode no + port 6379 + tcp-backlog 511 + timeout 0 + tcp-keepalive 300 + + # Persistence: AOF (fsync every second is the standard SignalR-backplane + # durability sweet spot — the backplane only needs to survive Redis + # restarts, not absolute zero loss). + appendonly yes + appendfsync everysec + auto-aof-rewrite-percentage 100 + auto-aof-rewrite-min-size 64mb + + # Reasonable defaults — let Redis pick most things. + maxmemory-policy allkeys-lru + maxmemory 256mb + + # Logging + loglevel notice +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fc-redis + namespace: fc-redis + labels: + app: fc-redis +spec: + replicas: 1 + strategy: + type: Recreate # RWO PVC; do not do rolling update + selector: + matchLabels: + app: fc-redis + template: + metadata: + labels: + app: fc-redis + spec: + securityContext: + runAsNonRoot: true + runAsUser: 999 # redis:7-alpine default uid + runAsGroup: 999 + fsGroup: 999 + containers: + - name: redis + image: redis:7-alpine + imagePullPolicy: IfNotPresent + command: ["redis-server", "/etc/redis/redis.conf"] + ports: + - name: redis + containerPort: 6379 + resources: + requests: + cpu: "50m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "384Mi" + volumeMounts: + - name: data + mountPath: /data + - name: config + mountPath: /etc/redis + readOnly: true + livenessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 2 + periodSeconds: 5 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: [ALL] + volumes: + - name: data + persistentVolumeClaim: + claimName: fc-redis-data + - name: config + configMap: + name: fc-redis-config +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: fc-redis +spec: + type: ClusterIP + selector: + app: fc-redis + ports: + - name: redis + port: 6379 + targetPort: 6379 + protocol: TCP