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:
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user