# One Valkey instance for ONE application - a cache or a queue backend, not a # shared database. There is deliberately no cluster mode, no replication and no # Ingress: a Valkey instance belongs on the cluster network, reachable through # its ClusterIP Service, and everything that would expose it to the internet is # absent rather than switched off. # # The two shapes this chart is meant for, and they are PAIRS of values, not two # independent switches: # # QUEUE (the chart default) - Sidekiq, Celery, anything whose entries are work # that must not vanish: # persistence.enabled: true # valkey.maxmemoryPolicy: noeviction # # CACHE - recomputable data only: # persistence.enabled: false # valkey.maxmemoryPolicy: allkeys-lru # # Mixing them is what this comment exists to prevent. A queue with # allkeys-lru throws away jobs under memory pressure while every application # stays green - one of the hypotheses in cluster-config#102, and cheaper to # prevent here than to diagnose there. And "persistence.enabled: false" together # with the default "noeviction" is almost never what anyone wants: a full cache # then starts REFUSING WRITES instead of dropping cold keys, and after a pod # restart it is empty anyway - so the strictness buys nothing and costs write # errors. # Scale the whole chart down without uninstalling it: scaleDown: true renders the # StatefulSet with replicas: 0 and takes precedence over valkey.replicas. The # PVC, the Secret, the Service and the NetworkPolicy stay in place; # scaleDown: false brings the workload back up. scaleDown: false secrets: # op:// path of the 1Password item holding this instance's password. # # The item must carry ONE field, named "auth-conf", whose value is a complete # Valkey configuration line: # # requirepass # # Not the bare password: the chart mounts this field as a file and the main # configuration "include"s it, so the password never appears in the process # list, in an environment variable, or anywhere Helm can see it. That is also # why no init container assembles the line - the vault is the only place the # secret exists in. # # A ready-made configuration line has one failure mode, and the chart closes # it: a misspelled key ("requirepas"), a stray quote or an empty field would # leave Valkey running WITHOUT a password and perfectly healthy - no render # error, no crash, an open server (the official image compiles protected-mode # OFF, so "no password" really does mean "anyone on the pod network"). The # init container therefore greps the mounted file for a line beginning with # "requirepass " and refuses to start the pod if it is absent. It never prints # the file. # # What this chart CANNOT cover: a change of the field's CONTENT inside the # vault. Helm never sees a secret materialized by the 1Password operator, so # there is nothing to hash. Repointing this value at a DIFFERENT item is # covered - checksum/secret-ref over the rendered OnePasswordItem rolls the # pod. After changing the password inside the item, restart by hand: # kubectl -n rollout restart statefulset -valkey-sfs # (operator.1password.io/auto-restart is NOT set: on a StatefulSet it is a # silent no-op - see README, "Config changes must restart the workload".) # # One side effect of the probes reading the same file, stated as UNVERIFIED # because it has not been measured: the auth file is a directory mount, so the # kubelet refreshes it within about a minute of the operator writing the new # password, while the running server still requires the old one. The liveness # probe then fails and the container is restarted - i.e. a rotated password # probably applies itself after roughly a minute of the instance being # unreachable. Do not rely on it; do the rollout restart above and know when # the interruption happens. authSecretRef: null valkey: # Replica count for this workload; 0 is allowed and scales it down. Values # above 1 are rejected by the schema: this chart runs ONE Valkey, and two # pods would fight over one ReadWriteOnce volume while clients would silently # get two different datasets through the Service. # The chart-global scaleDown flag takes precedence. replicas: 1 # Memory is the resource that decides what this chart does, because # valkey.maxmemory is derived from the computed memory LIMIT (see below). # 256Mi request x limitFactor 3 = 768Mi limit, of which Valkey may fill # maxmemoryFactor (0.75) = 576Mi with data. resources: # Limits are optional: memory and ephemeral-storage limits default to # limitFactor x the request, an explicitly set limit always wins, and a # CPU limit is only set when configured explicitly under "limits". limitFactor: 3 requests: # Valkey is single-threaded for command execution; 100m is enough for a # per-application cache and is a request, not a cap. cpu: 0.1 memory: 256Mi # The data volume is separate (persistence below). This covers the # container's own writable scratch space, which under this chart is only # /data when persistence is off. ephemeral-storage: 64Mi # The fraction of the container's memory LIMIT that Valkey may fill with data. # # Why a fraction and not the whole limit: maxmemory bounds the dataset, not # the process. Client output buffers, replication/AOF buffers and above all # the copy-on-write pages of the fork() that writes an RDB snapshot or rewrites # the AOF all live OUTSIDE maxmemory. Handing Valkey the full limit means the # kubelet OOM-kills the pod at the exact moment the eviction policy was # supposed to take over - which is the failure this value exists to prevent. maxmemoryFactor: 0.75 # maxmemory as an explicit Kubernetes quantity (e.g. "512Mi"), overriding the # derivation from maxmemoryFactor. null = derive it. # # A value ABOVE the computed memory limit ABORTS THE RENDER with both numbers # named. It is not clamped and not warned about: a maxmemory above the limit # means the kubelet's OOM kill decides what the eviction policy was configured # to decide, and that failure arrives as a restarted pod with no explanation. maxmemory: null # Valkey's maxmemory-policy. Default "noeviction" on purpose - see the pairs # at the top of this file. Under pressure Valkey then rejects writes with an # OOM error, which is loud in the application instead of silent in the data. # For a pure cache use "allkeys-lru". maxmemoryPolicy: noeviction # Extra Valkey configuration lines, appended verbatim to the rendered # configuration file (e.g. "appendfsync everysec" or "timeout 300"). # # The auth include is rendered AFTER this block, and in a Valkey configuration # the last occurrence of a directive wins - so nothing put here can switch the # password off, by accident or otherwise. It CAN override maxmemory and # maxmemory-policy, which then silently escapes the limit check above; set # valkey.maxmemory instead of writing a maxmemory line here. extraConfig: "" # All three probes run the same AUTHENTICATED "valkey-cli ping" as an exec # probe - Valkey serves no HTTP, so there is nothing to GET. The command is a # script from the chart's ConfigMap (templates/valkey.configmap.yaml, key # ping.sh), which is where the two properties that make it an assertion rather # than a formality are written down and commented: # # - the answer is asserted, not just the exit code ("grep -qx PONG") AND # valkey-cli is called with "-e" - because without -e it EXITS 0 on an # error reply, so a bare "valkey-cli ping" probe would report success while # printing "NOAUTH Authentication required."; # - the password goes through the VALKEYCLI_AUTH environment variable, read # out of the mounted auth file, never as an "-a" argument, which would put # it in the process list. # # Keeping the script in the ConfigMap (instead of a one-line shell command # inline here) has one consequence worth knowing: editing valkey.maxmemory or # any other configuration value changes the ConfigMap, and checksum/config # then rolls the pod - which it has to do anyway for the configuration itself. # # What these probes prove: the server accepted a connection, authenticated # with the password from the vault, and answered PONG. What they do not prove: # that the dataset is intact or that the application's keys are there. livenessProbe: exec: command: ["/bin/sh", "/etc/valkey/ping.sh"] periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: exec: command: ["/bin/sh", "/etc/valkey/ping.sh"] periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 # The startup probe gates liveness/readiness, so those need no # initialDelaySeconds. Startup budget = periodSeconds x failureThreshold = # 150s, which is what replaying a large append-only file from disk can cost; an # empty instance answers on the first check. startupProbe: exec: command: ["/bin/sh", "/etc/valkey/ping.sh"] periodSeconds: 5 timeoutSeconds: 5 failureThreshold: 30 securityContext: pod: # uid 999 / gid 1000 are NOT the repo's usual 20000/1000 and they are not # interchangeable: they are the "valkey" user the official image creates # (9.1/alpine/Dockerfile: "addgroup -S -g 1000 valkey; adduser -S -G # valkey -u 999 valkey" - gid 1000 because Alpine already uses gid 999). # The image sets no USER and starts as root; its entrypoint then steps # down to that user with setpriv, chown'ing /data on the way. Running the # pod as uid 999 directly skips that branch of the entrypoint - which is # the point (runAsNonRoot) and also why fsGroup has to match: without it # nothing would make the volume writable for the process. runAsNonRoot: true runAsUser: 999 runAsGroup: 1000 fsGroup: 1000 fsGroupChangePolicy: Always # RuntimeDefault applies the container runtime's default seccomp filter. # It sits in the pod context on purpose: from here it covers every # container in the pod INCLUDING the init container that validates the # auth file, while a container-level profile would have to be repeated for # each one (and silently missed on the next one added). seccompProfile: type: RuntimeDefault container: allowPrivilegeEscalation: false readOnlyRootFilesystem: true runAsNonRoot: true capabilities: drop: - ALL add: [] # Additive environment entries for the Valkey container, appended to what the # chart sets. Supported forms: value (rendered through tpl), secretKeyRef, # configMapKeyRef. # # Note what does NOT belong here: the password. The image's entrypoint appends # $VALKEY_EXTRA_FLAGS to the server command line, so a password routed through # this block would end up in the process list - exactly what the mounted # configuration file avoids. # # Additive means additive: an entry repeating a name the chart already sets is # applied at install time and LOST on an upgrade of an existing release # (Kubernetes merges env by name), while helm still reports success. env: [] persistence: # true = the dataset lives on a PVC and survives a pod restart. Off for a pure # cache; see the pairs at the top of this file. enabled: true # Append-only file instead of RDB snapshots. # # Why AOF is the default: for a queue, the question is not "is there a backup" # but "how much is lost when the pod dies". An RDB snapshot is written every # few minutes, so a crash loses every job accepted since the last one - and it # loses them silently, because the queue comes back looking healthy, only # shorter. AOF appends every write, so the loss window is the fsync interval. # The cost is disk writes and a slower start (the file is replayed), which the # startup budget above accounts for. # # With persistence.enabled: false this value has nothing to append to, and the # chart then renders "appendonly no" plus "save ''" so that a pod without a # volume writes nothing to its ephemeral storage at all. NOTES.txt prints # which of the two is active for the release at hand. appendonly: true # Small on purpose. On encrypted Longhorn volumes space is never returned to # the pool once written, so an oversized volume is paid for permanently - # start tight and grow (a PVC can grow; it can never shrink). size: 1Gi # longhorn-encrypted-ns: the data here is an application's cache or queue # content, which regularly contains the same payloads as its database. # A different class (or null for the cluster default) is possible, but then # the data is at rest unencrypted, which is a decision and not a detail. storageClass: longhorn-encrypted-ns networkPolicy: # On by default. This is a new chart with no existing installation that could # break on it, and a cache whose only boundary is a password is one leaked # credential away from being open to every pod in the cluster. # # READ THIS BEFORE RELYING ON IT: a NetworkPolicy is only a boundary if the # cluster's CNI enforces policies. Where it does not, the API server still # accepts the object and everything looks configured while nothing is # filtered - an unenforced policy is a false sense of security, not a weak # one. Verify on the cluster (try to connect from an unrelated namespace) # rather than reading it off this value. enabled: true # Namespaces whose pods may reach this instance in addition to the release's # own namespace. Empty means: own namespace only. # # Matched by the "kubernetes.io/metadata.name" label, which Kubernetes sets on # every namespace itself (>= 1.21), so no labelling work is needed - but also # no typo protection: a namespace name that does not exist matches nothing and # is silently ineffective. allowedNamespaces: []