First Commit

This commit is contained in:
Corrado Mulas
2026-07-23 12:09:41 +02:00
parent 22a5ad862b
commit a407903a8d
24 changed files with 3163 additions and 2 deletions

63
templates/NOTES.txt Normal file
View File

@@ -0,0 +1,63 @@
Native Redis Cluster deployed: {{ .Values.cluster.shards }} writable shards,
{{ .Values.cluster.replicasPerShard }} replica(s) per shard,
{{ include "redis-cluster.nodeCount" . }} total Redis instances.
Every shard's persistent members have REQUIRED hostname anti-affinity. A shard
cannot place two copies on one Kubernetes worker.
{{- if .Values.auth.enabled }}
Get the password:
kubectl get secret -n {{ .Release.Namespace }} {{ include "redis-cluster.secretName" . }} \
-o jsonpath='{.data.{{ include "redis-cluster.secretKey" . }}}' | base64 -d
{{- end }}
{{- if .Values.redisInsight.enabled }}
RedisInsight:
{{- if .Values.redisInsight.ingress.enabled }}
{{ ternary "https" "http" .Values.redisInsight.ingress.tls.enabled }}://{{ .Values.redisInsight.ingress.host }}
{{- else }}
kubectl port-forward -n {{ .Release.Namespace }} service/{{ include "redis-cluster.redisInsightName" . }} 5540:80
http://127.0.0.1:5540
{{- end }}
{{- end }}
Cluster-aware client startup endpoint:
{{ include "redis-cluster.fullname" . }}.{{ .Release.Namespace }}.svc:{{ .Values.redis.port }}
{{- if .Values.externalAccess.enabled }}
External cluster-aware client startup endpoint:
kubectl get service -n {{ .Release.Namespace }} \
{{ include "redis-cluster.fullname" . }}-external \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}{.status.loadBalancer.ingress[0].hostname}'
Use that returned address on port {{ .Values.externalAccess.port }}.
The external bootstrap LoadBalancer is discovery only. Redis advertises one
dynamic LoadBalancer VIP per member on the same port, so MOVED/ASK redirects
remain routable from outside Kubernetes without fixed addresses or port fan-out.
{{- end }}
Check all 16384 slots and primary/replica assignments:
kubectl exec -n {{ .Release.Namespace }} \
{{ include "redis-cluster.fullname" . }}-s0-n0-0 -c redis -- \
redis-cli --cluster check 127.0.0.1:{{ .Values.redis.port }}
Do not use a standalone or Sentinel client. The client must support Redis
Cluster redirects and connect to the per-node hostnames returned in the slot map.
StatefulSets use updateStrategy=OnDelete so independent members cannot all roll
at once. Follow the one-member-at-a-time upgrade procedure in README.md.
{{- if .Values.metrics.enabled }}
Metrics: http://<pod>:{{ .Values.metrics.port }}/metrics (redis_exporter).
{{- if not .Values.metrics.serviceMonitor.enabled }}
Set metrics.serviceMonitor.enabled=true (plus the correct selector labels) when
using Prometheus Operator.
{{- end }}
{{- end }}

154
templates/_helpers.tpl Normal file
View File

@@ -0,0 +1,154 @@
{{/* Chart name */}}
{{- define "redis-cluster.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/* Fully qualified app name */}}
{{- define "redis-cluster.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/* Chart label */}}
{{- define "redis-cluster.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/* Common labels */}}
{{- define "redis-cluster.labels" -}}
helm.sh/chart: {{ include "redis-cluster.chart" . }}
{{ include "redis-cluster.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{/* Selector labels */}}
{{- define "redis-cluster.selectorLabels" -}}
app.kubernetes.io/name: {{ include "redis-cluster.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
Headless service DNS name. Deliberately stop at .svc: the cluster DNS suffix is
not necessarily cluster.local, and Kubernetes expands this portable name using
the pod's search domains.
*/}}
{{- define "redis-cluster.headlessFqdn" -}}
{{ include "redis-cluster.fullname" . }}-headless.{{ .Release.Namespace }}.svc
{{- end -}}
{{/* Service account name */}}
{{- define "redis-cluster.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{- default (include "redis-cluster.fullname" .) .Values.serviceAccount.name -}}
{{- else -}}
{{- default "default" .Values.serviceAccount.name -}}
{{- end -}}
{{- end -}}
{{/* Secret name holding the password */}}
{{- define "redis-cluster.secretName" -}}
{{- if .Values.auth.existingSecret -}}
{{- .Values.auth.existingSecret -}}
{{- else -}}
{{- include "redis-cluster.fullname" . -}}
{{- end -}}
{{- end -}}
{{/* Secret key holding the password */}}
{{- define "redis-cluster.secretKey" -}}
{{- if .Values.auth.existingSecret -}}
{{- .Values.auth.existingSecretKey -}}
{{- else -}}
redis-password
{{- end -}}
{{- end -}}
{{/*
Password: explicit value wins; otherwise reuse the one already in the cluster
(so upgrades don't rotate it); otherwise generate.
*/}}
{{- define "redis-cluster.password" -}}
{{- if .Values.auth.password -}}
{{- .Values.auth.password -}}
{{- else -}}
{{- $existing := lookup "v1" "Secret" .Release.Namespace (include "redis-cluster.fullname" .) -}}
{{- if and $existing $existing.data (index $existing.data "redis-password") -}}
{{- index $existing.data "redis-password" | b64dec -}}
{{- else -}}
{{- randAlphaNum 24 -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/* Total Redis nodes: shards * (one primary + replicas per shard). */}}
{{- define "redis-cluster.nodeCount" -}}
{{- mul (int .Values.cluster.shards) (add (int .Values.cluster.replicasPerShard) 1) -}}
{{- end -}}
{{/* Balanced bootstrap Service used by cluster-aware clients. */}}
{{- define "redis-cluster.bootstrapServiceName" -}}
{{- if .Values.externalAccess.enabled -}}
{{- printf "%s-external" (include "redis-cluster.fullname" .) -}}
{{- else -}}
{{- include "redis-cluster.fullname" . -}}
{{- end -}}
{{- end -}}
{{/* RedisInsight resource name. */}}
{{- define "redis-cluster.redisInsightName" -}}
{{- printf "%s-insight" (include "redis-cluster.fullname" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/* RedisInsight labels deliberately do not match the Redis pod selector. */}}
{{- define "redis-cluster.redisInsightLabels" -}}
helm.sh/chart: {{ include "redis-cluster.chart" . }}
app.kubernetes.io/name: redisinsight
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: insight
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{- define "redis-cluster.redisInsightSelectorLabels" -}}
app.kubernetes.io/name: redisinsight
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: insight
{{- end -}}
{{/* Environment shared by Redis lifecycle scripts. */}}
{{- define "redis-cluster.commonEnv" -}}
- name: STS_NAME
value: {{ include "redis-cluster.fullname" . | quote }}
- name: HEADLESS_SERVICE
value: {{ include "redis-cluster.headlessFqdn" . | quote }}
- name: NODE_COUNT
value: {{ include "redis-cluster.nodeCount" . | quote }}
- name: SHARD_COUNT
value: {{ .Values.cluster.shards | quote }}
- name: REPLICAS_PER_SHARD
value: {{ .Values.cluster.replicasPerShard | quote }}
- name: REDIS_PORT
value: {{ .Values.redis.port | quote }}
- name: CLUSTER_BUS_PORT
value: {{ .Values.cluster.busPort | quote }}
{{- if .Values.auth.enabled }}
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "redis-cluster.secretName" . }}
key: {{ include "redis-cluster.secretKey" . }}
- name: REDISCLI_AUTH
valueFrom:
secretKeyRef:
name: {{ include "redis-cluster.secretName" . }}
key: {{ include "redis-cluster.secretKey" . }}
{{- end }}
{{- end -}}

381
templates/configmap.yaml Normal file
View File

@@ -0,0 +1,381 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "redis-cluster.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.labels" . | nindent 4 }}
data:
redis.conf: |
port {{ .Values.redis.port }}
dir /data
appendonly yes
appendfsync everysec
save ""
protected-mode no
tcp-keepalive 60
repl-diskless-sync yes
replica-read-only yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-port {{ .Values.cluster.busPort }}
cluster-node-timeout {{ .Values.cluster.nodeTimeoutMilliseconds }}
cluster-require-full-coverage {{ ternary "yes" "no" .Values.cluster.requireFullCoverage }}
cluster-allow-reads-when-down {{ ternary "yes" "no" .Values.cluster.allowReadsWhenDown }}
cluster-migration-barrier {{ .Values.cluster.migrationBarrier }}
cluster-preferred-endpoint-type hostname
{{- if .Values.redis.maxmemory }}
maxmemory {{ .Values.redis.maxmemory }}
maxmemory-policy {{ .Values.redis.maxmemoryPolicy }}
{{- end }}
{{- with .Values.redis.extraConfig }}
{{- . | nindent 4 }}
{{- end }}
start-redis.sh: |
#!/bin/sh
set -eu
POD_FQDN="$(hostname).${HEADLESS_SERVICE}"
if [ "$EXTERNAL_ACCESS_ENABLED" = "true" ]; then
ANNOUNCE_HOSTNAME="$(cat /external-endpoint/hostname)"
fi
/scripts/repair-node-addresses.sh
CONF=/etc/redis-runtime/redis.conf
cp /etc/redis-ro/redis.conf "$CONF"
{
echo "cluster-announce-hostname $ANNOUNCE_HOSTNAME"
echo "cluster-announce-human-nodename $(hostname)"
echo "cluster-announce-port $ANNOUNCE_PORT"
echo "cluster-announce-bus-port $CLUSTER_BUS_PORT"
if [ -n "${REDIS_PASSWORD:-}" ]; then
echo "requirepass $REDIS_PASSWORD"
echo "masterauth $REDIS_PASSWORD"
fi
} >> "$CONF"
# Only the designated coordinator attempts first-time cluster creation.
# The bootstrap runs beside Redis because every node must be listening
# before CLUSTER MEET can form the topology.
if [ "$POD_FQDN" = "$COORDINATOR_HOST" ]; then
/scripts/bootstrap-cluster.sh &
/scripts/reconcile-topology.sh &
fi
exec redis-server "$CONF"
discover-external-endpoint.sh: |
#!/bin/sh
set -eu
token_file=/var/run/secrets/kubernetes.io/serviceaccount/token
ca_file=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
api="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT_HTTPS}"
url="${api}/api/v1/namespaces/${POD_NAMESPACE}/services/${EXTERNAL_SERVICE_NAME}"
while :; do
response="$(curl --fail --silent --show-error --cacert "$ca_file" \
-H "Authorization: Bearer $(cat "$token_file")" "$url" 2>/dev/null || true)"
endpoint="$(printf '%s\n' "$response" |
sed -n 's/.*"ip":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)"
if [ -z "$endpoint" ]; then
endpoint="$(printf '%s\n' "$response" |
sed -n 's/.*"hostname":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)"
fi
if [ -n "$endpoint" ]; then
printf '%s\n' "$endpoint" > /external-endpoint/hostname
echo "Discovered external endpoint $endpoint for $EXTERNAL_SERVICE_NAME"
exit 0
fi
echo "Waiting for LoadBalancer endpoint on $EXTERNAL_SERVICE_NAME"
sleep 2
done
start-redisinsight.sh: |
#!/bin/sh
set -eu
RI_REDIS_HOST="$(cat /bootstrap-endpoint/hostname)"
export RI_REDIS_HOST
echo "Starting RedisInsight with discovered Redis bootstrap endpoint $RI_REDIS_HOST"
exec "$@"
repair-node-addresses.sh: |
#!/bin/sh
set -u
# nodes.conf persists node IDs and membership on the PVC, but its cluster
# bus endpoints include pod IPs. Refresh those IPs using each member's
# unique human nodename before Redis starts so a full reschedule recovers
# independently of the client endpoint Redis advertises.
nodes=/data/nodes.conf
[ -s "$nodes" ] || exit 0
resolve_host() {
host=$1
if command -v getent >/dev/null 2>&1; then
getent hosts "$host" 2>/dev/null | awk 'NR == 1 { print $1 }'
else
nslookup "$host" 2>/dev/null |
awk '/^Address: / && $2 !~ /#/ { address=$2 } END { print address }'
fi
}
while :; do
resolved=0
old_ifs=$IFS; IFS=','
for host in $ALL_NODE_HOSTS; do
[ -n "$(resolve_host "$host")" ] && resolved=$((resolved + 1))
done
IFS=$old_ifs
[ "$resolved" = "$NODE_COUNT" ] && break
echo "Waiting for all $NODE_COUNT persisted member hostnames to resolve ($resolved ready)"
sleep 2
done
old_ifs=$IFS; IFS=','
for assignment in $NODE_ADDRESS_ASSIGNMENTS; do
pod_name=${assignment%%=*}
internal_host=${assignment#*=}
ip="$(resolve_host "$internal_host")"
[ -n "$ip" ] || continue
awk -v target="$pod_name" -v new_ip="$ip" '
{
metadata_count = split($2, metadata, ",")
nodename = ""
for (i = 3; i <= metadata_count; i++) {
split(metadata[i], auxiliary, "=")
if (auxiliary[1] == "nodename") nodename = auxiliary[2]
}
if (nodename == target) {
split(metadata[1], bus, "@")
split(bus[1], endpoint, ":")
suffix = substr($2, length(metadata[1]) + 1)
$2 = new_ip ":" endpoint[2] "@" bus[2] suffix
}
print
}
' "$nodes" > "${nodes}.new" && mv "${nodes}.new" "$nodes"
done
IFS=$old_ifs
bootstrap-cluster.sh: |
#!/bin/sh
set -u
cli() {
redis-cli --no-auth-warning "$@"
}
resolve_host() {
host=$1
if command -v getent >/dev/null 2>&1; then
getent hosts "$host" 2>/dev/null | awk 'NR == 1 { print $1 }'
else
nslookup "$host" 2>/dev/null |
awk '/^Address: / && $2 !~ /#/ { address=$2 } END { print address }'
fi
}
node_value() {
host=$1 field=$2
cli -h "$host" -p "$REDIS_PORT" cluster info 2>/dev/null |
tr -d '\r' | awk -F: -v field="$field" '$1 == field { print $2 }'
}
echo "Redis Cluster bootstrap coordinator: $COORDINATOR_HOST"
while :; do
all_ready=true
old_ifs=$IFS; IFS=','
for host in $ALL_NODE_HOSTS; do
if [ "$(cli -h "$host" -p "$REDIS_PORT" ping 2>/dev/null)" != PONG ]; then
all_ready=false
break
fi
done
IFS=$old_ifs
$all_ready && break
sleep 2
done
if [ "$(node_value "$COORDINATOR_HOST" cluster_state)" = ok ]; then
echo "Redis Cluster already healthy; bootstrap is a no-op"
exit 0
fi
# Never overwrite a partially configured or previously initialized
# cluster. That state needs repair, not a destructive second bootstrap.
pristine=true
old_ifs=$IFS; IFS=','
for host in $ALL_NODE_HOSTS; do
known="$(node_value "$host" cluster_known_nodes)"
assigned="$(node_value "$host" cluster_slots_assigned)"
if [ "$known" != 1 ] || [ "$assigned" != 0 ]; then
pristine=false
break
fi
done
IFS=$old_ifs
if ! $pristine; then
echo "Cluster has existing membership or slots but is not healthy; refusing to reinitialize it" >&2
exit 0
fi
echo "Creating a ${SHARD_COUNT}-shard cluster from ${NODE_COUNT} pristine nodes"
old_ifs=$IFS; IFS=','
for host in $ALL_NODE_HOSTS; do
[ "$host" = "$COORDINATOR_HOST" ] && continue
ip="$(resolve_host "$host")"
if [ -z "$ip" ]; then
echo "Could not resolve $host" >&2
exit 1
fi
cli -h "$COORDINATOR_HOST" -p "$REDIS_PORT" \
cluster meet "$ip" "$REDIS_PORT" "$CLUSTER_BUS_PORT" >/dev/null || exit 1
done
IFS=$old_ifs
# Wait until every node has learned the complete membership before roles
# are assigned. This avoids CLUSTER REPLICATE racing gossip propagation.
while :; do
complete=true
old_ifs=$IFS; IFS=','
for host in $ALL_NODE_HOSTS; do
[ "$(node_value "$host" cluster_known_nodes)" = "$NODE_COUNT" ] || complete=false
done
IFS=$old_ifs
$complete && break
sleep 1
done
shard=0
old_ifs=$IFS; IFS=','
for primary in $PRIMARY_HOSTS; do
start=$((16384 * shard / SHARD_COUNT))
end=$((16384 * (shard + 1) / SHARD_COUNT - 1))
echo "Assigning slots $start-$end to shard $shard ($primary)"
cli -h "$primary" -p "$REDIS_PORT" cluster addslotsrange "$start" "$end" >/dev/null || exit 1
shard=$((shard + 1))
done
IFS=$old_ifs
old_ifs=$IFS; IFS=','
for assignment in $REPLICA_ASSIGNMENTS; do
replica=${assignment%%=*}
primary=${assignment#*=}
primary_id="$(cli -h "$primary" -p "$REDIS_PORT" cluster myid 2>/dev/null | tr -d '\r')"
[ -n "$primary_id" ] || exit 1
echo "Attaching $replica as a replica of $primary"
cli -h "$replica" -p "$REDIS_PORT" cluster replicate "$primary_id" >/dev/null || exit 1
done
IFS=$old_ifs
# Do not declare success merely because slots are covered. Wait for every
# intended replica relationship and for every member to see a healthy
# cluster, which prevents an early restart from racing topology gossip.
while :; do
converged=true
old_ifs=$IFS; IFS=','
for assignment in $REPLICA_ASSIGNMENTS; do
replica=${assignment%%=*}
role="$(cli -h "$replica" -p "$REDIS_PORT" role 2>/dev/null | head -n1)"
[ "$role" = slave ] || converged=false
done
for host in $ALL_NODE_HOSTS; do
[ "$(node_value "$host" cluster_state)" = ok ] || converged=false
done
IFS=$old_ifs
$converged && break
sleep 1
done
echo "Redis Cluster created: all 16384 slots covered"
reconcile-topology.sh: |
#!/bin/sh
set -u
# Redis correctly promotes replicas but does not automatically fail back.
# Restore the canonical member-0 primaries after a failed worker returns,
# keeping the three current primaries distributed across workers.
cli() { redis-cli --no-auth-warning "$@"; }
cluster_state() {
cli -h "$COORDINATOR_HOST" -p "$REDIS_PORT" cluster info 2>/dev/null |
tr -d '\r' | awk -F: '$1 == "cluster_state" { print $2 }'
}
while :; do
[ -f /etc/redis-runtime/terminating ] && exit 0
if [ "$(cluster_state)" = ok ]; then
old_ifs=$IFS; IFS=','
for primary_identity in $PRIMARY_HOSTS; do
role="$(cli -h "$primary_identity" -p "$REDIS_PORT" role 2>/dev/null | head -n1)"
if [ "$role" = slave ]; then
link="$(cli -h "$primary_identity" -p "$REDIS_PORT" info replication 2>/dev/null |
tr -d '\r' | awk -F: '$1 == "master_link_status" { print $2 }')"
if [ "$link" = up ]; then
echo "Restoring canonical primary role to $primary_identity"
cli -h "$primary_identity" -p "$REDIS_PORT" cluster failover >/dev/null 2>&1 || true
i=0
while [ "$i" -lt 20 ]; do
role="$(cli -h "$primary_identity" -p "$REDIS_PORT" role 2>/dev/null | head -n1)"
[ "$role" = master ] && break
sleep 1
i=$((i + 1))
done
while [ "$(cluster_state)" != ok ]; do sleep 1; done
fi
fi
done
IFS=$old_ifs
fi
sleep 30
done
startup-redis.sh: |
#!/bin/sh
[ "$(redis-cli -t 3 -p "$REDIS_PORT" --no-auth-warning ping 2>/dev/null)" = PONG ]
liveness-redis.sh: |
#!/bin/sh
resp="$(redis-cli -t 3 -p "$REDIS_PORT" --no-auth-warning ping 2>&1 || true)"
case "$resp" in
PONG|*LOADING*|*CLUSTERDOWN*) exit 0 ;;
*) echo "$resp"; exit 1 ;;
esac
readiness-redis.sh: |
#!/bin/sh
[ "$(redis-cli -t 3 -p "$REDIS_PORT" --no-auth-warning ping 2>/dev/null)" = PONG ] || exit 1
state="$(redis-cli -t 3 -p "$REDIS_PORT" --no-auth-warning cluster info 2>/dev/null |
tr -d '\r' | awk -F: '$1 == "cluster_state" { print $2 }')"
[ "$state" = ok ] || { echo "cluster_state=$state"; exit 1; }
prestop-redis.sh: |
#!/bin/sh
# During an intentional restart, ask a healthy replica to take over first.
# Replica identities remain on a different Kubernetes node because shard
# anti-affinity is tied to persistent member identity, not current role.
: > /etc/redis-runtime/terminating
role="$(redis-cli -p "$REDIS_PORT" --no-auth-warning role 2>/dev/null | head -n1 || true)"
[ "$role" = master ] || exit 0
node_id="$(redis-cli -p "$REDIS_PORT" --no-auth-warning cluster myid 2>/dev/null | tr -d '\r')"
[ -n "$node_id" ] || exit 0
endpoint="$(redis-cli -p "$REDIS_PORT" --no-auth-warning cluster replicas "$node_id" 2>/dev/null |
awk '$3 !~ /fail/ && $8 == "connected" { print $2; exit }')"
[ -n "$endpoint" ] || exit 0
address=${endpoint%%@*}
metadata=${endpoint#*,}
if [ "$metadata" != "$endpoint" ] && [ -n "$metadata" ]; then
replica_host=$metadata
else
replica_host=${address%:*}
fi
replica_port=${address##*:}
echo "Requesting graceful failover to $replica_host:$replica_port"
redis-cli -h "$replica_host" -p "$replica_port" --no-auth-warning cluster failover >/dev/null 2>&1 || exit 0
i=0
while [ "$i" -lt 15 ]; do
role="$(redis-cli -p "$REDIS_PORT" --no-auth-warning role 2>/dev/null | head -n1 || true)"
[ "$role" != master ] && exit 0
sleep 1
i=$((i + 1))
done
exit 0

View File

@@ -0,0 +1,37 @@
{{- if .Values.externalAccess.enabled }}
{{- $fullname := include "redis-cluster.fullname" . }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ $fullname }}-endpoint-reader
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.labels" . | nindent 4 }}
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get"]
resourceNames:
- {{ $fullname }}-external
{{- range $shard := until (int .Values.cluster.shards) }}
{{- range $member := until (int (add (int $.Values.cluster.replicasPerShard) 1)) }}
- {{ printf "%s-s%d-n%d-x" $fullname $shard $member | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ $fullname }}-endpoint-reader
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.labels" . | nindent 4 }}
subjects:
- kind: ServiceAccount
name: {{ include "redis-cluster.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ $fullname }}-endpoint-reader
{{- end }}

View File

@@ -0,0 +1,51 @@
{{- if .Values.networkPolicy.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "redis-cluster.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "redis-cluster.selectorLabels" . | nindent 6 }}
policyTypes:
- Ingress
ingress:
# Intra-release traffic: client protocol is also used for replication and
# key migration; the cluster bus carries gossip and failover elections.
- from:
- podSelector:
matchLabels:
{{- include "redis-cluster.selectorLabels" . | nindent 14 }}
ports:
- port: {{ .Values.redis.port }}
- port: {{ .Values.cluster.busPort }}
# Client traffic.
{{- if .Values.networkPolicy.allowExternal }}
- ports:
- port: {{ .Values.redis.port }}
{{- else if .Values.networkPolicy.extraIngress }}
- from:
{{- toYaml .Values.networkPolicy.extraIngress | nindent 8 }}
ports:
- port: {{ .Values.redis.port }}
{{- end }}
{{- if .Values.metrics.enabled }}
# Metrics scraping.
{{- if .Values.networkPolicy.metricsFromNamespaces }}
- from:
{{- range .Values.networkPolicy.metricsFromNamespaces }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . }}
{{- end }}
ports:
- port: {{ .Values.metrics.port }}
{{- else }}
- ports:
- port: {{ .Values.metrics.port }}
{{- end }}
{{- end }}
{{- end }}

14
templates/pdb.yaml Normal file
View File

@@ -0,0 +1,14 @@
{{- if .Values.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "redis-cluster.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.labels" . | nindent 4 }}
spec:
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
selector:
matchLabels:
{{- include "redis-cluster.selectorLabels" . | nindent 6 }}
{{- end }}

View File

@@ -0,0 +1,87 @@
{{- if and .Values.redisInsight.enabled .Values.redisInsight.ingress.enabled }}
{{- if not .Values.redisInsight.ingress.host }}
{{- fail "redisInsight.ingress.host is required when RedisInsight ingress is enabled" }}
{{- end }}
{{- if and .Values.redisInsight.ingress.tls.enabled (not .Values.redisInsight.ingress.tls.credentialName) }}
{{- fail "redisInsight.ingress.tls.credentialName is required when TLS is enabled" }}
{{- end }}
{{- if and .Values.redisInsight.ingress.tls.certificate.create (not .Values.redisInsight.ingress.tls.enabled) }}
{{- fail "redisInsight.ingress.tls.enabled must be true when certificate.create is true" }}
{{- end }}
{{- if and .Values.redisInsight.ingress.tls.certificate.create (not .Values.redisInsight.ingress.tls.certificate.issuerName) }}
{{- fail "redisInsight.ingress.tls.certificate.issuerName is required when certificate.create is true" }}
{{- end }}
{{- if and .Values.redisInsight.ingress.tls.enabled .Values.redisInsight.ingress.tls.certificate.create }}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ include "redis-cluster.redisInsightName" . }}-tls
namespace: {{ default .Release.Namespace .Values.redisInsight.ingress.tls.certificate.secretNamespace }}
labels:
{{- include "redis-cluster.redisInsightLabels" . | nindent 4 }}
spec:
secretName: {{ .Values.redisInsight.ingress.tls.credentialName }}
issuerRef:
name: {{ .Values.redisInsight.ingress.tls.certificate.issuerName }}
kind: {{ .Values.redisInsight.ingress.tls.certificate.issuerKind }}
commonName: {{ .Values.redisInsight.ingress.host | quote }}
dnsNames:
- {{ .Values.redisInsight.ingress.host | quote }}
---
{{- end }}
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: {{ include "redis-cluster.redisInsightName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.redisInsightLabels" . | nindent 4 }}
spec:
selector:
{{- toYaml .Values.redisInsight.ingress.gatewaySelector | nindent 4 }}
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- {{ .Values.redisInsight.ingress.host | quote }}
{{- if .Values.redisInsight.ingress.tls.enabled }}
tls:
httpsRedirect: true
{{- end }}
{{- if .Values.redisInsight.ingress.tls.enabled }}
- port:
number: 443
name: https
protocol: HTTPS
hosts:
- {{ .Values.redisInsight.ingress.host | quote }}
tls:
mode: SIMPLE
credentialName: {{ .Values.redisInsight.ingress.tls.credentialName }}
{{- end }}
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: {{ include "redis-cluster.redisInsightName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.redisInsightLabels" . | nindent 4 }}
spec:
hosts:
- {{ .Values.redisInsight.ingress.host | quote }}
gateways:
- {{ include "redis-cluster.redisInsightName" . }}
http:
- match:
- uri:
prefix: /
timeout: {{ .Values.redisInsight.ingress.requestTimeout }}
route:
- destination:
host: {{ include "redis-cluster.redisInsightName" . }}
port:
number: 80
{{- end }}

220
templates/redisinsight.yaml Normal file
View File

@@ -0,0 +1,220 @@
{{- if .Values.redisInsight.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "redis-cluster.redisInsightName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.redisInsightLabels" . | nindent 4 }}
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
selector:
{{- include "redis-cluster.redisInsightSelectorLabels" . | nindent 4 }}
{{- if .Values.redisInsight.persistence.enabled }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "redis-cluster.redisInsightName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.redisInsightLabels" . | nindent 4 }}
{{- with .Values.redisInsight.persistence.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
{{- toYaml .Values.redisInsight.persistence.accessModes | nindent 4 }}
{{- with .Values.redisInsight.persistence.storageClass }}
storageClassName: {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.redisInsight.persistence.size }}
{{- end }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "redis-cluster.redisInsightName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.redisInsightLabels" . | nindent 4 }}
spec:
replicas: 1
# RedisInsight stores its connection database under /data. Recreate avoids
# two pods contending for the same ReadWriteOnce volume during an upgrade.
strategy:
type: Recreate
selector:
matchLabels:
{{- include "redis-cluster.redisInsightSelectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "redis-cluster.redisInsightSelectorLabels" . | nindent 8 }}
annotations:
sidecar.istio.io/inject: "false"
{{- with .Values.redisInsight.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- if and .Values.externalAccess.enabled (not .Values.redisInsight.connection.host) }}
serviceAccountName: {{ include "redis-cluster.serviceAccountName" . }}
automountServiceAccountToken: false
{{- end }}
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
{{- if and .Values.externalAccess.enabled (not .Values.redisInsight.connection.host) }}
initContainers:
- name: discover-bootstrap-endpoint
image: "{{ .Values.externalAccess.endpointDiscovery.image.repository }}:{{ .Values.externalAccess.endpointDiscovery.image.tag }}"
imagePullPolicy: {{ .Values.externalAccess.endpointDiscovery.image.pullPolicy }}
command: ["/bin/sh", "/scripts/discover-external-endpoint.sh"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: EXTERNAL_SERVICE_NAME
value: {{ include "redis-cluster.bootstrapServiceName" . | quote }}
resources:
{{- toYaml .Values.externalAccess.endpointDiscovery.resources | nindent 12 }}
volumeMounts:
- name: scripts
mountPath: /scripts
readOnly: true
- name: bootstrap-endpoint
mountPath: /external-endpoint
- name: kube-api-access
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
{{- end }}
containers:
- name: redisinsight
image: "{{ .Values.redisInsight.image.repository }}:{{ .Values.redisInsight.image.tag }}"
imagePullPolicy: {{ .Values.redisInsight.image.pullPolicy }}
{{- if and .Values.externalAccess.enabled (not .Values.redisInsight.connection.host) }}
# Preserve the image CMD as arguments; this wrapper only injects the
# dynamically discovered LoadBalancer endpoint before exec.
command: ["/scripts/start-redisinsight.sh"]
{{- end }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
env:
- name: RI_APP_HOST
value: "0.0.0.0"
- name: RI_APP_PORT
value: {{ .Values.redisInsight.port | quote }}
# These variables register a connection inside RedisInsight only;
# they do not alter the Redis cluster or its Kubernetes resources.
- name: RI_REDIS_HOST
value: {{ default (include "redis-cluster.bootstrapServiceName" .) .Values.redisInsight.connection.host | quote }}
- name: RI_REDIS_PORT
value: {{ default (ternary .Values.externalAccess.port .Values.redis.port .Values.externalAccess.enabled) .Values.redisInsight.connection.port | quote }}
- name: RI_REDIS_ALIAS
value: {{ .Values.redisInsight.connection.alias | quote }}
- name: RI_REDIS_TLS
value: {{ .Values.redisInsight.connection.tls | quote }}
{{- if .Values.auth.enabled }}
- name: RI_REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "redis-cluster.secretName" . }}
key: {{ include "redis-cluster.secretKey" . }}
{{- end }}
- name: RI_STDOUT_LOGGER
value: "true"
ports:
- name: http
containerPort: {{ .Values.redisInsight.port }}
protocol: TCP
startupProbe:
httpGet:
path: /api/health/
port: http
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 60
readinessProbe:
httpGet:
path: /api/health/
port: http
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
livenessProbe:
httpGet:
path: /api/health/
port: http
periodSeconds: 20
timeoutSeconds: 3
failureThreshold: 5
resources:
{{- toYaml .Values.redisInsight.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: /data
{{- if and .Values.externalAccess.enabled (not .Values.redisInsight.connection.host) }}
- name: scripts
mountPath: /scripts
readOnly: true
- name: bootstrap-endpoint
mountPath: /bootstrap-endpoint
readOnly: true
{{- end }}
volumes:
- name: data
{{- if .Values.redisInsight.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "redis-cluster.redisInsightName" . }}
{{- else }}
emptyDir: {}
{{- end }}
{{- if and .Values.externalAccess.enabled (not .Values.redisInsight.connection.host) }}
- name: scripts
configMap:
name: {{ include "redis-cluster.fullname" . }}
defaultMode: 0555
- name: bootstrap-endpoint
emptyDir: {}
- name: kube-api-access
projected:
defaultMode: 0444
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt
{{- end }}
{{- with .Values.redisInsight.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.redisInsight.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}

12
templates/secret.yaml Normal file
View File

@@ -0,0 +1,12 @@
{{- if and .Values.auth.enabled (not .Values.auth.existingSecret) }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "redis-cluster.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.labels" . | nindent 4 }}
type: Opaque
data:
redis-password: {{ include "redis-cluster.password" . | b64enc | quote }}
{{- end }}

View File

@@ -0,0 +1,75 @@
{{- if .Values.externalAccess.enabled }}
{{- $root := . }}
{{- $fullname := include "redis-cluster.fullname" . }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $fullname }}-external
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.labels" . | nindent 4 }}
redis-cluster.external-role: bootstrap
{{- with .Values.externalAccess.bootstrap.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: LoadBalancer
externalTrafficPolicy: {{ .Values.externalAccess.externalTrafficPolicy }}
allocateLoadBalancerNodePorts: {{ .Values.externalAccess.allocateLoadBalancerNodePorts }}
{{- with .Values.externalAccess.loadBalancerClass }}
loadBalancerClass: {{ . | quote }}
{{- end }}
{{- with .Values.externalAccess.loadBalancerSourceRanges }}
loadBalancerSourceRanges:
{{- toYaml . | nindent 4 }}
{{- end }}
selector:
{{- include "redis-cluster.selectorLabels" . | nindent 4 }}
ports:
- name: tcp-redis
port: {{ .Values.externalAccess.port }}
targetPort: tcp-redis
appProtocol: redis
{{- range $shard := until (int .Values.cluster.shards) }}
{{- range $member := until (int (add (int $.Values.cluster.replicasPerShard) 1)) }}
{{- $memberName := printf "%s-s%d-n%d" $fullname $shard $member | trunc 63 | trimSuffix "-" }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $memberName }}-x
namespace: {{ $.Release.Namespace }}
labels:
{{- include "redis-cluster.labels" $root | nindent 4 }}
redis-cluster.external-role: member
redis-cluster.shard: {{ $shard | quote }}
redis-cluster.member: {{ $member | quote }}
{{- with $.Values.externalAccess.memberServices.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: LoadBalancer
externalTrafficPolicy: {{ $.Values.externalAccess.externalTrafficPolicy }}
allocateLoadBalancerNodePorts: {{ $.Values.externalAccess.allocateLoadBalancerNodePorts }}
{{- with $.Values.externalAccess.loadBalancerClass }}
loadBalancerClass: {{ . | quote }}
{{- end }}
{{- with $.Values.externalAccess.loadBalancerSourceRanges }}
loadBalancerSourceRanges:
{{- toYaml . | nindent 4 }}
{{- end }}
selector:
{{- include "redis-cluster.selectorLabels" $root | nindent 4 }}
redis-cluster.shard: {{ $shard | quote }}
redis-cluster.member: {{ $member | quote }}
ports:
- name: tcp-redis
port: {{ $.Values.externalAccess.port }}
targetPort: tcp-redis
appProtocol: redis
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,22 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "redis-cluster.fullname" . }}-headless
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.labels" . | nindent 4 }}
spec:
clusterIP: None
# Pods must resolve each other during bootstrap, before they are Ready.
publishNotReadyAddresses: true
selector:
{{- include "redis-cluster.selectorLabels" . | nindent 4 }}
ports:
- name: tcp-redis
port: {{ .Values.redis.port }}
targetPort: tcp-redis
appProtocol: redis
- name: tcp-cluster
port: {{ .Values.cluster.busPort }}
targetPort: tcp-cluster
appProtocol: redis

24
templates/service.yaml Normal file
View File

@@ -0,0 +1,24 @@
{{/* Any node is a valid startup node. Cluster-aware clients fetch the slot map
and then connect directly to the stable per-member hostnames Redis advertises. */}}
apiVersion: v1
kind: Service
metadata:
name: {{ include "redis-cluster.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.labels" . | nindent 4 }}
spec:
type: ClusterIP
selector:
{{- include "redis-cluster.selectorLabels" . | nindent 4 }}
ports:
- name: tcp-redis
port: {{ .Values.redis.port }}
targetPort: tcp-redis
appProtocol: redis
{{- if .Values.metrics.enabled }}
- name: http-metrics
port: {{ .Values.metrics.port }}
targetPort: http-metrics
appProtocol: http
{{- end }}

View File

@@ -0,0 +1,14 @@
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "redis-cluster.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: false
{{- end }}

View File

@@ -0,0 +1,20 @@
{{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "redis-cluster.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis-cluster.labels" . | nindent 4 }}
{{- with .Values.metrics.serviceMonitor.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
selector:
matchLabels:
{{- include "redis-cluster.selectorLabels" . | nindent 6 }}
endpoints:
- port: http-metrics
interval: {{ .Values.metrics.serviceMonitor.interval }}
scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }}
{{- end }}

311
templates/statefulset.yaml Normal file
View File

@@ -0,0 +1,311 @@
{{- $root := . -}}
{{- $fullname := include "redis-cluster.fullname" . -}}
{{- if gt (len $fullname) 53 -}}
{{- fail (printf "release fullname %q is too long; use fullnameOverride with at most 53 characters" $fullname) -}}
{{- end -}}
{{- if ge (int .Values.cluster.replicasPerShard) (int .Values.cluster.shards) -}}
{{- fail "cluster.replicasPerShard must be lower than cluster.shards so crossed placement never puts a shard replica with its own primary" -}}
{{- end -}}
{{- $headless := include "redis-cluster.headlessFqdn" . -}}
{{- $nodeCount := include "redis-cluster.nodeCount" . -}}
{{- $allHosts := list -}}
{{- $primaryHosts := list -}}
{{- $assignments := list -}}
{{- $addressAssignments := list -}}
{{- range $shard := until (int .Values.cluster.shards) -}}
{{- range $member := until (int (add (int $.Values.cluster.replicasPerShard) 1)) -}}
{{- $memberName := printf "%s-s%d-n%d" $fullname $shard $member | trunc 63 | trimSuffix "-" -}}
{{- $podName := printf "%s-0" $memberName -}}
{{- $host := printf "%s-0.%s" $memberName $headless -}}
{{- $allHosts = append $allHosts $host -}}
{{- $addressAssignments = append $addressAssignments (printf "%s=%s" $podName $host) -}}
{{- if eq $member 0 -}}
{{- $primaryHosts = append $primaryHosts $host -}}
{{- else -}}
{{- $primaryName := printf "%s-s%d-n0" $fullname $shard | trunc 63 | trimSuffix "-" -}}
{{- $primaryHost := printf "%s-0.%s" $primaryName $headless -}}
{{- $assignments = append $assignments (printf "%s=%s" $host $primaryHost) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $coordinator := first $primaryHosts -}}
{{- range $shard := until (int .Values.cluster.shards) }}
{{- range $member := until (int (add (int $.Values.cluster.replicasPerShard) 1)) }}
{{- $memberName := printf "%s-s%d-n%d" $fullname $shard $member | trunc 63 | trimSuffix "-" }}
{{- $memberIndex := add (mul $shard (add (int $.Values.cluster.replicasPerShard) 1)) $member }}
{{- $announceHostname := printf "%s-0.%s" $memberName $headless }}
{{- $externalServiceName := printf "%s-x" $memberName }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ $memberName }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "redis-cluster.labels" $root | nindent 4 }}
redis-cluster.shard: {{ $shard | quote }}
redis-cluster.member: {{ $member | quote }}
spec:
serviceName: {{ $fullname }}-headless
replicas: 1
podManagementPolicy: Parallel
# Separate one-member StatefulSets would otherwise all roll concurrently.
# OnDelete makes upgrades explicit and safe; see README.md.
updateStrategy:
type: OnDelete
selector:
matchLabels:
{{- include "redis-cluster.selectorLabels" $root | nindent 6 }}
redis-cluster.shard: {{ $shard | quote }}
redis-cluster.member: {{ $member | quote }}
template:
metadata:
labels:
{{- include "redis-cluster.selectorLabels" $root | nindent 8 }}
redis-cluster.shard: {{ $shard | quote }}
redis-cluster.member: {{ $member | quote }}
{{- with $.Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") $root | sha256sum }}
{{- if not $.Values.istio.injectSidecar }}
sidecar.istio.io/inject: "false"
{{- else }}
proxy.istio.io/config: '{ "holdApplicationUntilProxyStarts": true }'
{{- end }}
{{- with $.Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
serviceAccountName: {{ include "redis-cluster.serviceAccountName" $root }}
automountServiceAccountToken: false
terminationGracePeriodSeconds: {{ $.Values.terminationGracePeriodSeconds }}
{{- with $.Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $.Values.priorityClassName }}
priorityClassName: {{ . }}
{{- end }}
securityContext:
{{- toYaml $.Values.podSecurityContext | nindent 8 }}
affinity:
{{- if gt $member 0 }}
# Cross replicas onto the worker holding another shard's canonical
# primary. For 3x1 this produces A:P0+R2, B:P1+R0, C:P2+R1.
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
{{- include "redis-cluster.selectorLabels" $root | nindent 18 }}
redis-cluster.shard: {{ mod (add $shard $member) (int $.Values.cluster.shards) | quote }}
redis-cluster.member: "0"
topologyKey: kubernetes.io/hostname
{{- end }}
podAntiAffinity:
# Hard invariants: copies of one shard never share a worker, and
# equivalent member identities are spread across workers. In the
# canonical topology, this permits at most one primary per worker.
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
{{- include "redis-cluster.selectorLabels" $root | nindent 18 }}
redis-cluster.shard: {{ $shard | quote }}
topologyKey: kubernetes.io/hostname
- labelSelector:
matchLabels:
{{- include "redis-cluster.selectorLabels" $root | nindent 18 }}
redis-cluster.member: {{ $member | quote }}
topologyKey: kubernetes.io/hostname
{{- with $.Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $.Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $.Values.topologySpreadConstraints }}
topologySpreadConstraints:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if $.Values.externalAccess.enabled }}
initContainers:
- name: discover-external-endpoint
image: "{{ $.Values.externalAccess.endpointDiscovery.image.repository }}:{{ $.Values.externalAccess.endpointDiscovery.image.tag }}"
imagePullPolicy: {{ $.Values.externalAccess.endpointDiscovery.image.pullPolicy }}
command: ["/bin/sh", "/scripts/discover-external-endpoint.sh"]
securityContext:
{{- toYaml $.Values.containerSecurityContext | nindent 12 }}
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: EXTERNAL_SERVICE_NAME
value: {{ $externalServiceName | quote }}
resources:
{{- toYaml $.Values.externalAccess.endpointDiscovery.resources | nindent 12 }}
volumeMounts:
- name: scripts
mountPath: /scripts
- name: external-endpoint
mountPath: /external-endpoint
- name: kube-api-access
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
{{- end }}
containers:
- name: redis
image: "{{ $.Values.image.repository }}:{{ $.Values.image.tag }}"
imagePullPolicy: {{ $.Values.image.pullPolicy }}
command: ["/bin/sh", "/scripts/start-redis.sh"]
securityContext:
{{- toYaml $.Values.containerSecurityContext | nindent 12 }}
env:
{{- include "redis-cluster.commonEnv" $root | nindent 12 }}
- name: COORDINATOR_HOST
value: {{ $coordinator | quote }}
- name: ALL_NODE_HOSTS
value: {{ join "," $allHosts | quote }}
- name: PRIMARY_HOSTS
value: {{ join "," $primaryHosts | quote }}
- name: REPLICA_ASSIGNMENTS
value: {{ join "," $assignments | quote }}
- name: NODE_ADDRESS_ASSIGNMENTS
value: {{ join "," $addressAssignments | quote }}
- name: ANNOUNCE_HOSTNAME
value: {{ $announceHostname | quote }}
- name: ANNOUNCE_PORT
value: {{ ternary $.Values.externalAccess.port $.Values.redis.port $.Values.externalAccess.enabled | quote }}
- name: EXTERNAL_ACCESS_ENABLED
value: {{ $.Values.externalAccess.enabled | quote }}
ports:
- name: tcp-redis
containerPort: {{ $.Values.redis.port }}
- name: tcp-cluster
containerPort: {{ $.Values.cluster.busPort }}
startupProbe:
exec:
command: ["/bin/sh", "/scripts/startup-redis.sh"]
initialDelaySeconds: {{ $.Values.probes.redis.startup.initialDelaySeconds }}
periodSeconds: {{ $.Values.probes.redis.startup.periodSeconds }}
timeoutSeconds: {{ $.Values.probes.redis.startup.timeoutSeconds }}
failureThreshold: {{ $.Values.probes.redis.startup.failureThreshold }}
livenessProbe:
exec:
command: ["/bin/sh", "/scripts/liveness-redis.sh"]
periodSeconds: {{ $.Values.probes.redis.liveness.periodSeconds }}
timeoutSeconds: {{ $.Values.probes.redis.liveness.timeoutSeconds }}
failureThreshold: {{ $.Values.probes.redis.liveness.failureThreshold }}
readinessProbe:
exec:
command: ["/bin/sh", "/scripts/readiness-redis.sh"]
periodSeconds: {{ $.Values.probes.redis.readiness.periodSeconds }}
timeoutSeconds: {{ $.Values.probes.redis.readiness.timeoutSeconds }}
failureThreshold: {{ $.Values.probes.redis.readiness.failureThreshold }}
lifecycle:
preStop:
exec:
command: ["/bin/sh", "/scripts/prestop-redis.sh"]
resources:
{{- toYaml $.Values.redis.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: /data
- name: scripts
mountPath: /scripts
- name: config
mountPath: /etc/redis-ro
- name: redis-runtime
mountPath: /etc/redis-runtime
{{- if $.Values.externalAccess.enabled }}
- name: external-endpoint
mountPath: /external-endpoint
readOnly: true
{{- end }}
{{- if $.Values.metrics.enabled }}
- name: metrics
image: "{{ $.Values.metrics.image.repository }}:{{ $.Values.metrics.image.tag }}"
imagePullPolicy: {{ $.Values.metrics.image.pullPolicy }}
securityContext:
{{- toYaml $.Values.containerSecurityContext | nindent 12 }}
env:
- name: REDIS_ADDR
value: "redis://localhost:{{ $.Values.redis.port }}"
{{- if $.Values.auth.enabled }}
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "redis-cluster.secretName" $root }}
key: {{ include "redis-cluster.secretKey" $root }}
{{- end }}
ports:
- name: http-metrics
containerPort: {{ $.Values.metrics.port }}
livenessProbe:
tcpSocket:
port: http-metrics
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
tcpSocket:
port: http-metrics
periodSeconds: 15
resources:
{{- toYaml $.Values.metrics.resources | nindent 12 }}
{{- end }}
volumes:
- name: scripts
configMap:
name: {{ $fullname }}
defaultMode: 0555
- name: config
configMap:
name: {{ $fullname }}
items:
- key: redis.conf
path: redis.conf
- name: redis-runtime
emptyDir: {}
{{- if $.Values.externalAccess.enabled }}
- name: external-endpoint
emptyDir: {}
- name: kube-api-access
projected:
defaultMode: 0444
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt
{{- end }}
{{- if not $.Values.persistence.enabled }}
- name: data
emptyDir: {}
{{- end }}
{{- if $.Values.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
{{- with $.Values.persistence.annotations }}
annotations:
{{- toYaml . | nindent 10 }}
{{- end }}
spec:
accessModes:
{{- toYaml $.Values.persistence.accessModes | nindent 10 }}
{{- with $.Values.persistence.storageClass }}
storageClassName: {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ $.Values.persistence.size }}
{{- end }}
{{- end }}
{{- end }}