Merge pull request 'Add a Redis chart for toolshed's managed cache add-on' (#1) from add-redis-chart into main

Reviewed-on: http://gitea.100.90.248.118.nip.io/mukul/devops-infra-helm-charts/pulls/1
This commit is contained in:
mukul
2026-09-09 07:30:04 +00:00
5 changed files with 307 additions and 0 deletions
@@ -0,0 +1,50 @@
# Redis backing toolshed's managed cache add-on.
#
# Deployed as shared infrastructure in its own namespace rather than inside
# the toolshed namespace, so it is addressed over cluster DNS like any other
# platform component and its lifecycle is independent of the application
# that happens to be its first consumer:
#
# redis.redis.svc.cluster.local:6379
#
# The admin password comes from Vault through External Secrets — see
# devops-infra-argo-config/secretstores/toolshed-redis-credentials.yaml. The
# Secret must exist before this pod can start; a missing Secret leaves the
# init container in CreateContainerConfigError rather than failing in a way
# that explains itself.
#
# Read values.yaml's `config` block before changing anything about
# authentication here. The absence of `requirepass` is deliberate and
# security-relevant, not an oversight.
fullnameOverride: redis
image:
repository: redis
tag: "7-alpine"
pullPolicy: IfNotPresent
existingSecret: redis-credentials
persistence:
enabled: true
storageClass: local-path
# Holds the ACL file and nothing else worth keeping — snapshotting is off
# (see config.save). 1Gi is already far more than needed; local-path
# cannot resize in place, so it is sized up front rather than tightly.
size: 1Gi
config:
# The node has 8GB and was at its ceiling before Postgres was added; the
# demo apps were scaled to zero to make room for that. 48mb is a real
# cache for a handful of small internal tools and costs little.
maxmemory: 48mb
maxmemoryPolicy: allkeys-lru
save: ""
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
memory: 96Mi
+23
View File
@@ -0,0 +1,23 @@
apiVersion: v2
name: redis
description: |
Single-instance Redis for this homelab, backing toolshed's managed cache
add-on (internal/dbprovision) — toolshed provisions a per-app ACL user
scoped to its own key prefix on request.
Hand-written rather than vendoring Bitnami's chart, for the same reason
the sibling postgresql chart is: Broadcom has been retiring and freezing
images behind that repo (claude.md infra issue #4, where it broke Contour
twice), and Redis publishes no official Helm chart of its own.
Authentication is defined entirely by the ACL file, with no requirepass.
That is not a style choice — see values.yaml, where the reasoning is
recorded alongside the setting it explains. Getting it wrong leaves the
server open to unauthenticated access after its first restart.
Not highly available and not intended to be. One replica, one PVC, no
replication, no sentinel. On a single-node cluster those would be
theatre.
type: application
version: 0.1.0
appVersion: "7"
@@ -0,0 +1,23 @@
{{- $name := .Values.fullnameOverride | default "redis" -}}
# ClusterIP only. Nothing outside the cluster should reach Redis, and there
# is no Ingress here on purpose — Contour terminates HTTP, and exposing
# Redis's wire protocol through it is neither possible nor wanted.
#
# Consumers address this as:
# {{ $name }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }}
apiVersion: v1
kind: Service
metadata:
name: {{ $name }}
namespace: {{ .Release.Namespace }}
labels:
app: {{ $name }}
spec:
type: ClusterIP
selector:
app: {{ $name }}
ports:
- name: redis
port: {{ .Values.service.port }}
targetPort: redis
protocol: TCP
@@ -0,0 +1,127 @@
{{- $name := .Values.fullnameOverride | default "redis" -}}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ $name }}
namespace: {{ .Release.Namespace }}
labels:
app: {{ $name }}
spec:
serviceName: {{ $name }}
replicas: 1
selector:
matchLabels:
app: {{ $name }}
template:
metadata:
labels:
app: {{ $name }}
spec:
securityContext:
# The official image runs as the redis user (uid 999 on the Alpine
# variant). fsGroup makes the provisioned volume group-writable so
# Redis can write the ACL file it is given — without it ACL SAVE
# fails at provisioning time with a permission error.
fsGroup: 999
terminationGracePeriodSeconds: 30
initContainers:
# Seeds the ACL file with the default (admin) user on first boot
# only. Redis will not start with an --aclfile that does not exist,
# and the default user has to be defined there rather than by
# requirepass — see the long note in values.yaml for why that
# distinction is a security property and not a preference.
#
# Never overwrites an existing file. That file is rewritten by ACL
# SAVE every time toolshed provisions an app user, so recreating it
# on every pod start would silently delete every provisioned user
# and lock those apps out — the exact failure this whole design
# exists to prevent, reintroduced from the other end.
#
# Consequence worth knowing: rotating the admin password in Vault
# does NOT propagate here, because this only ever runs against a
# missing file. Rotating means `ACL SETUSER default >newpassword`
# followed by `ACL SAVE` against the running server.
- name: seed-acl
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.existingSecret }}
key: {{ .Values.secretKeys.password }}
command:
- sh
- -c
- |
set -e
if [ -f /data/users.acl ]; then
echo "ACL file already present; leaving it alone."
exit 0
fi
echo "user default on >$REDIS_PASSWORD ~* &* +@all" > /data/users.acl
chmod 600 /data/users.acl
echo "Seeded ACL file with the default user."
volumeMounts:
- name: data
mountPath: /data
containers:
- name: redis
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- redis-server
- --aclfile
- /data/users.acl
- --maxmemory
- {{ .Values.config.maxmemory | quote }}
- --maxmemory-policy
- {{ .Values.config.maxmemoryPolicy | quote }}
- --save
- {{ .Values.config.save | quote }}
ports:
- name: redis
containerPort: 6379
protocol: TCP
# Authenticated probes: with the ACL file in place an
# unauthenticated PING is correctly refused with NOAUTH, so a
# bare `redis-cli ping` would mark a perfectly healthy server as
# failing. Run through a shell so the environment expands —
# Kubernetes does not substitute $(VAR) inside exec probe
# commands.
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.existingSecret }}
key: {{ .Values.secretKeys.password }}
readinessProbe:
exec:
command: ["sh", "-c", 'redis-cli --no-auth-warning -a "$REDIS_PASSWORD" ping | grep -q PONG']
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
exec:
command: ["sh", "-c", 'redis-cli --no-auth-warning -a "$REDIS_PASSWORD" ping | grep -q PONG']
initialDelaySeconds: 20
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 6
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: /data
{{- if .Values.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: {{ .Values.persistence.storageClass | quote }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- end }}
+84
View File
@@ -0,0 +1,84 @@
# Chart defaults. Real configuration lives in
# helm-overrides/k8s-admin-prd-ase1/redis/custom-values.yaml.
fullnameOverride: redis
image:
# Pulled from Docker Hub, like every other infra component here (gitea,
# vault, harbor, postgresql). The base-images mirror in Harbor exists to
# remove Docker Hub from the *application build* path — it is not in play
# for platform components.
repository: redis
tag: "7-alpine"
pullPolicy: IfNotPresent
# Name of the Secret holding the admin password. Created by External
# Secrets from Vault, not by this chart — a chart that generates its own
# password regenerates it on every render, which would rewrite the ACL file
# and lock every already-provisioned app out of its own data.
existingSecret: redis-credentials
secretKeys:
password: password
service:
port: 6379
persistence:
enabled: true
# local-path-provisioner, this cluster's default StorageClass. Small: this
# holds the ACL file and (if enabled) an RDB snapshot, not a dataset of
# any size — maxmemory below is the real ceiling on what Redis will hold.
# The volume is not resizable in place with this provisioner, so it is
# sized up front.
storageClass: local-path
size: 1Gi
config:
# ACL FILE, NOT requirepass. This distinction is load-bearing and easy to
# "simplify" into a security hole, so it is written down here rather than
# left to be rediscovered:
#
# toolshed provisions per-app users with ACL SETUSER, and persists them
# with ACL SAVE (internal/dbprovision.EnsureRedisUser) — without that
# save, every provisioned user is lost on the next restart and every app
# using Redis fails to authenticate with credentials that still look
# valid. ACL SAVE requires an aclfile; that is why one is configured.
#
# But ACL SAVE also writes the *default* user's state to that file. With
# `requirepass` set and the default user defined only by it, the saved
# entry comes back as `user default on nopass ~* &* +@all` — and after
# the next restart the ACL file wins, leaving Redis accepting
# UNAUTHENTICATED connections with full access. Verified directly, not
# inferred: with requirepass the restarted server answered an
# unauthenticated PING with PONG and served a key.
#
# Defining the default user in the ACL file instead (seeded by the init
# container, see the StatefulSet) keeps its password across every
# subsequent ACL SAVE — the same restart then correctly answers
# `NOAUTH Authentication required.`
#
# If you ever add `requirepass` here, you reintroduce that hole.
maxmemory: 48mb
# allkeys-lru, because this backs a connection kind literally called
# "cache" and eviction under pressure is that contract. An app using
# Redis as its only copy of something wants noeviction instead — at
# which case writes start failing when full rather than data silently
# disappearing. Neither is safe for every use; this one matches the name.
maxmemoryPolicy: allkeys-lru
# Snapshotting off. What must survive a restart is the ACL file, which is
# written by ACL SAVE independently of RDB/AOF. Cached values are by
# definition reconstructible, and on a node at its memory ceiling a
# background save's copy-on-write spike is a real risk for no benefit.
save: ""
# Tuned for a node with 8GB total that is already near its ceiling. The
# request is what the scheduler reserves; the limit is sized above
# maxmemory so Redis hits its own eviction policy rather than being
# OOM-killed by the kernel, which loses the whole instance instead of the
# coldest keys.
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
memory: 96Mi