Skip to content

Valkey Addon

A Redis-compatible key-value store managed by the platform: one StatefulSet per app, plan-controlled replica count, single addressable endpoint via a regular ClusterIP Service, and a REDIS_URL env var injected straight into the app pod via envFrom.

The wire protocol is Redis-compatible (Valkey is a fork) so any existing Redis client connects unchanged — the connection URI scheme stays redis://.

How it works

sequenceDiagram
  participant U as Operator
  participant CP as Control Plane
  participant DB as Postgres (app_addons)
  participant K as Kubernetes
  participant V as Valkey StatefulSet
  participant App as App pod

  U->>CP: POST /v1/apps/{id}/addons { type:"valkey", plan:"standard" }
  CP->>CP: validate plan + type, plan_to_resources / valkey_plan_config
  CP->>DB: INSERT app_addons (status='creating')
  CP->>K: ensure_valkey(plan)  → StatefulSet + ClusterIP Service
  K->>V: bootstrap pods (valkey/valkey:7-alpine, --requirepass)
  K->>K: Secret {name}-auth (uri, host, port, password)
  CP->>K: ensure_redis_url_secret → Secret app-{id}-redis-url
  loop until ready
    CP->>K: get StatefulSet.status.readyReplicas
    CP->>CP: parse_valkey_status → Creating / Ready / Failed
  end
  K->>App: envFrom: { secretRef: { name: app-{id}-redis-url } }
  App->>K: read REDIS_URL on start

Plans

Plan Replicas Memory Persistence
free 1 64Mi none
standard 1 256Mi rdb_daily
pro 3 (HA) 1Gi rdb_aof

Unknown plans silently fall back to free so a typo can't push the StatefulSet into a bucket the cluster can't satisfy. Persistence strings are advisory today (the StatefulSet doesn't yet wire a PVC); cycle 3 will mount a volume for rdb_* plans.

Service shape — ClusterIP, NOT headless

A key difference from Redis: Valkey is exposed as a regular ClusterIP Service. Apps connect to a single endpoint ({name}.{namespace}.svc.cluster.local:6379); the replication and failover behaviour is internal to Valkey. Redis ships as a headless Service (clusterIP: None) so clients pick a specific pod — that path is preserved for backwards-compatibility with the existing Redis addon, but Valkey doesn't need it.

REDIS_URL secret

ensure_redis_url_secret server-side-applies a Secret named app-{app_id}-redis-url in the app's tenant namespace, with a single REDIS_URL key whose value is the full redis://:{password}@{name}.{namespace}.svc.cluster.local:6379 URI sourced from the {name}-auth Secret create_valkey already produced. The URI is not re-constructed — the canonical value lives in one place, in CNPG-style.

The app pod's PodSpec mounts it via:

envFrom:
  - secretRef:
      name: app-{app_id}-redis-url

Result: a process.env.REDIS_URL (or os.environ['REDIS_URL']) on the app side. Clients that prefer parsed creds can split it themselves — Redis client libraries accept the URI directly.

Status vocabulary

parse_valkey_status(ready, desired) projects StatefulSet readiness into the operator-facing vocabulary:

ready vs desired ValkeyStatus
ready == desired (both > 0) Ready
ready == 0 Creating
partial (0 < ready < desired) Failed

Failed is the loud signal — a replica died or the rollout stalled. The operator should kubectl describe statefulset in the tenant namespace.

Idempotency

Op Idempotency
POST /addons (same app_id, type=valkey) DB row rotates via ON CONFLICT (app_id, addon_type) DO UPDATE; StatefulSet rotates via ensure_valkey (get-then-create-or-patch on spec.replicas)
ensure_redis_url_secret Patch::Apply(...).force() with field manager paas-control-plane — refreshes if Valkey password rotates

Naming conventions (pinned by tests)

Resource Pattern Source
StatefulSet valkey-{tenant_id}-{app_id} (truncated at 53 chars) valkey::valkey_name
Auth Secret {name}-auth (single source of uri/password/host/port) valkey::create_valkey
App-side Secret app-{app_id}-redis-url valkey::redis_url_secret_name
Image valkey/valkey:7-alpine valkey::VALKEY_IMAGE_BASE

Failure modes

Symptom Likely cause Recovery
Creating for >5 min image pull, scheduling kubectl describe statefulset … in tenant ns
Failed (partial readiness) replica crashed, OOM, exec liveness failed kubectl logs the pod that's not ready; if OOM, bump plan to standard or pro
App restart-loops on missing REDIS_URL ensure_redis_url_secret never ran re-call POST /addons — server-side apply re-converges

Phase 2

  • PVC + persistence wiring — today the rdb_daily and rdb_aof plans don't actually persist; cycle 3 mounts a PVC and toggles the Valkey config flags.
  • Valkey Cluster mode — current pro plan runs 3 replicas but they're standalone; cluster sharding lands in a follow-up.
  • Eviction policy per plan — today every plan uses Valkey's default noeviction. The dashboard might surface a maxmemory-policy knob in cycle 3.
  • Postgres Addon — same lifecycle, same app_addons table, same naming conventions
  • Add-ons — the unified list view
  • Network Policiesallow-app-redis / allow-app-valkey is the per-app egress allow that lets pods reach the addon