Files
helm-charts/valkey/templates/valkey.sfs.yaml
T
jklein 813c796a7b
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 8s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Successful in 4s
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Successful in 11s
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Successful in 1m22s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 7s
Publish Valkey Helm Chart / Publish Valkey Helm Chart (push) Successful in 7s
feat(valkey): one Valkey instance per application (1.0.0+up9.1.2.alpine)
A new chart for a single Valkey instance per application, replacing the
shared cluster-redis whose separation rests on one ACL file in one
1Password field - a construction in which every change touches six
applications and which cost a month of Paperless standstill.

Shape: StatefulSet with one replica, ClusterIP Service, no cluster mode,
no Ingress and no value to add one.

The password arrives as a finished configuration line ("requirepass
<secret>") in the 1Password field auth-conf, is mounted as a file and
included by the main configuration - never in argv, never in the
environment. The include is rendered last, so nothing in extraConfig can
override it. Because a misspelled or empty field would leave an open
server that looks perfectly healthy, an init container greps the mounted
file for the line and refuses to start the pod without it; it never
prints the file.

maxmemory is derived from the computed memory limit (maxmemoryFactor,
default 0.75) and a configured value above that limit aborts the render:
otherwise the kubelet's OOM kill decides what the eviction policy was
configured to decide. Persistence and eviction are documented as pairs -
queue (PVC + noeviction, the default) and cache (emptyDir +
allkeys-lru) - and the combination "no persistence + noeviction" carries
a warning in values.yaml and in NOTES.txt.

Probes: valkey-cli ping as an exec probe, authenticated through
VALKEYCLI_AUTH read out of the mounted file, with "-e" and an assertion
on the answer PONG. Without -e valkey-cli exits 0 on an error reply
(valkey 9.1.2, src/valkey-cli.c: the config.set_errcode branch in
cliReadReply), which would make the probe unable to fail.

Hardening: uid 999 / gid 1000 - the image's own valkey user - with
fsGroup matching, runAsNonRoot, readOnlyRootFilesystem, all capabilities
dropped and seccompProfile RuntimeDefault in the pod context so the init
container is covered too. /data is the only writable path.

NetworkPolicy on by default (first chart in this repo with one), with a
switch and a list of additional namespaces; values.yaml names that it is
only a boundary where the CNI enforces policies.

Wiring: publish-valkey.yml, the chart-name choice list, ci/valkey
fixtures for the persistent variant, a plain docker-versioning Renovate
block, README chart row.
2026-09-27 11:24:55 +00:00

163 lines
8.0 KiB
YAML

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
spec:
replicas: {{ include "valkey.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.valkey.replicas) }}
serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-service
selector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
template:
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
annotations:
# Hash of the rendered ConfigMap: a changed configuration changes the pod
# template and thus restarts the pod. Without it a changed maxmemory or
# eviction policy would apply to nothing - Valkey reads its
# configuration once, at start, and a mounted file is not re-read.
checksum/config: {{ include (print $.Template.BasePath "/valkey.configmap.yaml") . | sha256sum }}
# Hash of the rendered OnePasswordItem. This covers the SOURCE of the
# password, not its content: repointing secrets.authSecretRef at a
# different vault item rolls the pod instead of leaving the old password
# in the running server. A change INSIDE the item cannot be covered -
# Helm never sees a secret the 1Password operator materializes (see
# values.yaml).
#
# operator.1password.io/auto-restart is deliberately NOT set: the
# operator only restarts Deployments, so on a StatefulSet it is a silent
# no-op that advertises a guarantee which does not exist.
checksum/secret-ref: {{ include (print $.Template.BasePath "/valkey.secret.yaml") . | sha256sum }}
spec:
automountServiceAccountToken: false
securityContext:
{{- include "valkey.podSecurityContext" .Values.valkey.securityContext | nindent 8 }}
initContainers:
# Validates the mounted auth file and nothing else. It does not compose
# the configuration line - that comes finished out of the vault - it only
# refuses to let the pod start when the line is missing, which is the one
# misconfiguration of this chart that would otherwise produce a healthy,
# password-less, network-reachable Valkey. The script, and the reason it
# never prints the file, are in templates/valkey.configmap.yaml.
#
# Same image as the server, so there is no second image to pin, no
# Renovate block to maintain for it and nothing extra to pull.
- name: {{ .Release.Name }}-{{ .Chart.Name }}-check-auth
image: "docker.io/valkey/valkey:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent
# command, not args: args would be handed to the image's entrypoint,
# which starts valkey-server for anything that looks like a config
# file or an option.
command:
- /bin/sh
- /etc/valkey/check-auth.sh
securityContext:
{{- include "valkey.containerSecurityContext" .Values.valkey.securityContext | nindent 12 }}
# Fixed and deliberately small: this container runs one grep. Not
# values-configurable - there is nothing here to tune.
resources:
requests:
cpu: 10m
memory: 16Mi
ephemeral-storage: 1Mi
limits:
cpu: 100m
memory: 32Mi
ephemeral-storage: 10Mi
volumeMounts:
- name: config
mountPath: /etc/valkey
readOnly: true
- name: auth
mountPath: /etc/valkey-auth
readOnly: true
containers:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
image: "docker.io/valkey/valkey:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent
# args, NOT command: the image's entrypoint is
# "tini -- docker-entrypoint.sh", and replacing it would lose tini as
# PID 1 (zombie reaping) and the entrypoint's umask. An argument
# ending in .conf makes the entrypoint start
# "valkey-server /etc/valkey/valkey.conf", which is exactly what is
# wanted. Note which branch of that entrypoint is NOT taken: running as
# uid 999 skips its root-only "chown /data + setpriv" step, so the
# volume is made writable by fsGroup instead.
args:
- /etc/valkey/valkey.conf
# The chart sets no environment variable of its own - everything the
# server is told comes from its configuration file, which is what keeps
# the password out of the environment. So the block is rendered only
# when the operator actually adds an entry, instead of leaving an empty
# "env:" (i.e. env: null) in the manifest.
{{- with .Values.valkey.env }}
env:
{{- include "valkey.env" (dict "root" $ "env" .) | nindent 12 }}
{{- end }}
ports:
- containerPort: 6379
protocol: TCP
name: valkey
# Probes, resources and both security contexts go through the
# null-safe helpers (#99): an erased block means "chart defaults
# apply" instead of a vanished probe or an unhardened container.
# Never wrap these in an "if" - a guard hides the erased case
# completely instead of rendering a visible null.
livenessProbe:
{{- include "valkey.livenessProbe" .Values.valkey.livenessProbe | nindent 12 }}
readinessProbe:
{{- include "valkey.readinessProbe" .Values.valkey.readinessProbe | nindent 12 }}
startupProbe:
{{- include "valkey.startupProbe" .Values.valkey.startupProbe | nindent 12 }}
resources:
{{- include "valkey.effectiveResources" .Values.valkey.resources | nindent 12 }}
securityContext:
{{- include "valkey.containerSecurityContext" .Values.valkey.securityContext | nindent 12 }}
volumeMounts:
# The image's WORKDIR. Everything Valkey writes goes here (the AOF
# directory, RDB snapshots), and it is the only writable path the
# server needs - which is what lets readOnlyRootFilesystem stay true
# without a single extra emptyDir.
- name: data
mountPath: /data
- name: config
mountPath: /etc/valkey
readOnly: true
# The auth file, mounted as a DIRECTORY rather than with subPath, so
# the kubelet keeps it up to date when the 1Password operator writes
# a new password into the Secret. That does not restart the server by
# itself (see the annotations above).
- name: auth
mountPath: /etc/valkey-auth
readOnly: true
restartPolicy: Always
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
{{- else }}
# persistence.enabled is false: the dataset lives in the pod and is
# gone with it. The configuration switches both AOF and the RDB save
# points off in that case, so this emptyDir stays empty - it exists
# because the image's WORKDIR has to be writable.
emptyDir: {}
{{- end }}
- name: config
configMap:
name: {{ .Release.Name }}-{{ .Chart.Name }}-configmap
- name: auth
secret:
secretName: {{ .Release.Name }}-{{ .Chart.Name }}-secret
items:
# The 1Password item's field name on the left, the file name on the
# right. Spelled out rather than mounting the whole Secret, so a
# missing field fails the pod with a named reference instead of
# producing a directory without the file.
- key: auth-conf
path: auth.conf