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
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.
161 lines
9.6 KiB
Plaintext
161 lines
9.6 KiB
Plaintext
{{- $replicas := include "valkey.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.valkey.replicas) -}}
|
|
{{- $limitBytes := include "valkey.memoryLimitBytes" .Values.valkey.resources -}}
|
|
{{- $maxmemory := include "valkey.maxmemoryBytes" (dict "resources" .Values.valkey.resources "maxmemory" .Values.valkey.maxmemory "maxmemoryFactor" .Values.valkey.maxmemoryFactor) -}}
|
|
Valkey {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
|
|
|
ACCESS — IN-CLUSTER ONLY
|
|
{{ .Release.Name }}-{{ .Chart.Name }}-service.{{ .Release.Namespace }}.svc.cluster.local:6379
|
|
|
|
There is no Ingress and no value to add one: this is one application's cache
|
|
or queue backend, and its only authentication is the password. Applications
|
|
address it with a redis:// URL (Valkey speaks the same protocol, and every
|
|
client library still calls the scheme redis://):
|
|
|
|
redis://:<password>@{{ .Release.Name }}-{{ .Chart.Name }}-service.{{ .Release.Namespace }}.svc.cluster.local:6379/0
|
|
|
|
The password belongs in the CONSUMING application's own vault item, where its
|
|
connection URL lives. Nothing here writes it there.
|
|
|
|
Check by hand from inside the namespace, with the password out of the pod's own
|
|
mounted file so it never reaches your shell history:
|
|
|
|
kubectl -n {{ .Release.Namespace }} exec {{ .Release.Name }}-{{ .Chart.Name }}-sfs-0 -- /bin/sh /etc/valkey/ping.sh && echo OK
|
|
|
|
THE 1PASSWORD ITEM MUST CARRY ONE FIELD, AND ITS NAME IS PART OF THE CHART
|
|
secrets.authSecretRef points at
|
|
{{ .Values.secrets.authSecretRef }}
|
|
The 1Password operator materializes that item as the Kubernetes Secret
|
|
{{ .Release.Name }}-{{ .Chart.Name }}-secret, with one Secret key per field of the item. This chart
|
|
reads exactly one field:
|
|
|
|
auth-conf a COMPLETE Valkey configuration line, not the bare
|
|
password:
|
|
|
|
requirepass <the password>
|
|
|
|
A differently named field produces no such key, and the pod then stays in
|
|
ContainerCreating with "couldn't find key auth-conf" in its events.
|
|
|
|
Why a whole configuration line instead of the password: the file is mounted and
|
|
"include"d by the configuration, so the password never appears in the process
|
|
list, in an environment variable, or in anything Helm renders. The cost is that
|
|
a typo in that line would be a Valkey WITHOUT a password — running, healthy,
|
|
and reachable from the whole pod network, because the official image compiles
|
|
protected-mode off. An init container therefore checks the mounted file for a
|
|
"requirepass " line before the server starts and refuses to let the pod run
|
|
otherwise. If the pod is stuck in Init:Error or Init:CrashLoopBackOff, read it:
|
|
|
|
kubectl -n {{ .Release.Namespace }} logs {{ .Release.Name }}-{{ .Chart.Name }}-sfs-0 -c {{ .Release.Name }}-{{ .Chart.Name }}-check-auth
|
|
|
|
It never prints the field's contents.
|
|
|
|
Watch the materialization with
|
|
|
|
kubectl -n {{ .Release.Namespace }} get onepassworditem {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
|
kubectl -n {{ .Release.Namespace }} get secret {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
|
|
|
THIS RELEASE IS CONFIGURED AS A {{ if .Values.persistence.enabled }}{{ if eq .Values.valkey.maxmemoryPolicy "noeviction" }}QUEUE{{ else }}PERSISTENT CACHE{{ end }}{{ else }}{{ if eq .Values.valkey.maxmemoryPolicy "noeviction" }}VOLATILE STORE — PROBABLY BY MISTAKE{{ else }}CACHE{{ end }}{{ end }}
|
|
persistence.enabled: {{ .Values.persistence.enabled }}
|
|
valkey.maxmemoryPolicy: {{ .Values.valkey.maxmemoryPolicy }}
|
|
memory limit (computed): {{ include "valkey.humanBytes" $limitBytes }} ({{ $limitBytes }} bytes)
|
|
maxmemory: {{ include "valkey.humanBytes" $maxmemory }} ({{ $maxmemory }} bytes){{ if kindIs "invalid" .Values.valkey.maxmemory }}, derived as maxmemoryFactor {{ .Values.valkey.maxmemoryFactor | default 0.75 }} x the limit{{ else }}, set explicitly in values and checked against the limit{{ end }}
|
|
{{ if .Values.persistence.enabled }}
|
|
{{- if eq .Values.valkey.maxmemoryPolicy "noeviction" }}
|
|
The combination this chart is built for. When memory runs out Valkey REFUSES
|
|
WRITES instead of discarding keys, so a full queue is an error in the
|
|
application rather than jobs that quietly disappeared.
|
|
{{- else }}
|
|
Persistence with an evicting policy: the data survives a restart but not
|
|
memory pressure. Fine for a cache that is expensive to rebuild — wrong for a
|
|
Sidekiq or Celery queue, which would lose jobs while staying green.
|
|
{{- end }}
|
|
{{- else }}
|
|
{{- if eq .Values.valkey.maxmemoryPolicy "noeviction" }}
|
|
READ THIS: no persistence AND noeviction is almost never intended. The data is
|
|
gone on every pod restart, so nothing is being protected — but once memory is
|
|
full, writes start FAILING instead of cold keys being dropped. For a cache set
|
|
valkey.maxmemoryPolicy: allkeys-lru; for a queue set persistence.enabled: true.
|
|
{{- else }}
|
|
Cache shape: the dataset lives in the pod only. Both AOF and the RDB save
|
|
points are switched off, so a restart starts from empty and nothing is written
|
|
to ephemeral storage.
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- if .Values.persistence.enabled }}
|
|
|
|
STORAGE
|
|
{{ .Release.Name }}-{{ .Chart.Name }}-pvc, {{ .Values.persistence.size }}, {{ if .Values.persistence.storageClass }}storage class "{{ .Values.persistence.storageClass }}"{{ else }}on the cluster's default storage class
|
|
(persistence.storageClass is unset — the default class is not encrypted){{ end }}.
|
|
Persistence mode: {{ if .Values.persistence.appendonly }}append-only file (every write appended; a crash loses at most
|
|
the last fsync interval){{ else }}RDB snapshots only (a crash loses every write since the last
|
|
snapshot, silently — the instance comes back healthy, only shorter){{ end }}.
|
|
|
|
helm uninstall does NOT delete this PVC, which is deliberate — the data
|
|
survives a reinstall of the same release name. It also means a release removed
|
|
and recreated under a DIFFERENT name starts on an empty volume while the old
|
|
one keeps occupying space. On encrypted Longhorn volumes that space is never
|
|
returned to the pool.
|
|
{{- end }}
|
|
{{ if eq $replicas "0" }}
|
|
SCALED TO ZERO
|
|
{{ if .Values.scaleDown }}scaleDown: true{{ else }}valkey.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so nothing answers on
|
|
the address above and every application configured against it will fail its own
|
|
checks. Nothing else is removed: Service and Secret stay{{ if .Values.persistence.enabled }}, the PVC keeps
|
|
the data{{ end }}{{ if .Values.networkPolicy.enabled }}, and the NetworkPolicy stays in force{{ end }}.
|
|
{{ end }}
|
|
CHANGING THE PASSWORD IS NOT COVERED BY A CHECKSUM
|
|
The pod template carries checksum/config over the configuration ConfigMap and
|
|
checksum/secret-ref over the rendered OnePasswordItem. So a changed
|
|
maxmemory/policy rolls the pod, and so does repointing secrets.authSecretRef at
|
|
a DIFFERENT vault item. What cannot be covered is a new password inside the
|
|
same item: Helm never sees the content of a Secret the 1Password operator
|
|
fills in. After changing it there:
|
|
|
|
kubectl -n {{ .Release.Namespace }} rollout restart statefulset {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
|
|
|
(operator.1password.io/auto-restart is not set on purpose — the operator
|
|
restarts Deployments only, so on a StatefulSet it would be a silent no-op.)
|
|
|
|
Unverified, and named so rather than promised: because the probes read the
|
|
password out of the same mounted file, the kubelet's refresh of that file
|
|
(roughly a minute) should make the liveness probe fail against the still-old
|
|
server and restart the container by itself. Do the rollout restart instead of
|
|
waiting for it.
|
|
{{ if .Values.networkPolicy.enabled }}
|
|
NETWORK POLICY IS ACTIVE
|
|
Ingress to port 6379 is allowed from namespace {{ .Release.Namespace }}{{ if .Values.networkPolicy.allowedNamespaces }} and from
|
|
{{ join ", " .Values.networkPolicy.allowedNamespaces }}{{ end }}. Egress is unrestricted (this instance opens no
|
|
outbound connection of its own).
|
|
|
|
It is only a boundary where the CNI enforces NetworkPolicies. Where it does
|
|
not, the object is accepted and nothing is filtered — so verify it instead of
|
|
reading it off the manifest:
|
|
|
|
kubectl -n <some-other-namespace> run t --rm -it --image=docker.io/valkey/valkey:{{ .Chart.AppVersion }} \
|
|
-- valkey-cli -h {{ .Release.Name }}-{{ .Chart.Name }}-service.{{ .Release.Namespace }} ping
|
|
|
|
That must hang or be refused. "NOAUTH Authentication required." means the
|
|
policy is not being enforced and the password is your only boundary.
|
|
{{- else }}
|
|
NO NETWORK POLICY
|
|
networkPolicy.enabled is false, so the password is the only thing between this
|
|
instance and every pod in the cluster. The official image compiles
|
|
protected-mode off, so there is no second line of defence behind it.
|
|
{{- end }}
|
|
|
|
WHAT A GREEN POD PROVES
|
|
All three probes run "valkey-cli -e ping" AUTHENTICATED with the password from
|
|
the mounted file and require the answer PONG. So Ready means: the server
|
|
accepted a connection, accepted that password, and answered.
|
|
|
|
Two things it does not mean. It does not say anything about the dataset — a
|
|
restarted instance whose AOF was lost answers PONG on an empty database. And
|
|
it does not say your application's keys are reachable: that depends on the
|
|
connection URL in the application's own vault item, which this chart neither
|
|
writes nor reads.
|
|
|
|
The probe asserts the answer (PONG), not just that the command returned, and
|
|
it passes "-e" so that valkey-cli reports an error reply through its exit code
|
|
at all — without it a bare "valkey-cli ping" exits 0 while printing
|
|
"NOAUTH Authentication required.", which would be a probe that cannot fail.
|