First Commit
This commit is contained in:
13
.helmignore
Normal file
13
.helmignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Patterns to ignore when building packages.
|
||||||
|
.DS_Store
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
# Argo CD examples live next to the chart but are not rendered by it.
|
||||||
|
argocd/
|
||||||
21
Chart.yaml
Normal file
21
Chart.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: redis-cluster
|
||||||
|
description: >-
|
||||||
|
Sharded, highly available Redis Cluster with multiple writable primaries,
|
||||||
|
per-shard replicas, automatic failover, and Prometheus metrics.
|
||||||
|
type: application
|
||||||
|
version: 0.4.0
|
||||||
|
appVersion: "7.4"
|
||||||
|
kubeVersion: ">=1.25.0-0"
|
||||||
|
home: https://git.mulas.me/corrado/redis-cluster
|
||||||
|
sources:
|
||||||
|
- https://git.mulas.me/corrado/redis-cluster
|
||||||
|
- https://github.com/redis/redis
|
||||||
|
keywords:
|
||||||
|
- redis
|
||||||
|
- redis-cluster
|
||||||
|
- sharding
|
||||||
|
- high-availability
|
||||||
|
- cache
|
||||||
|
maintainers:
|
||||||
|
- name: corrado.mulas
|
||||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2026 corrado.
|
Copyright (c) 2026 Corrado Mulas <tlc (at) mulas.me>.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
|||||||
987
README.md
987
README.md
@@ -1,3 +1,988 @@
|
|||||||
# redis-cluster
|
# redis-cluster
|
||||||
|
|
||||||
Helm chart for Kubernetes Redis sharded cluster with cross-readers
|
A production-oriented Helm chart for a native, sharded Redis Cluster on
|
||||||
|
Kubernetes. It creates multiple independently writable shard primaries, one or
|
||||||
|
more readable failover replicas for every shard, strict cross-node placement,
|
||||||
|
persistent storage, optional external cluster-aware access, Prometheus metrics,
|
||||||
|
and an optional RedisInsight UI.
|
||||||
|
|
||||||
|
This chart deploys Redis Cluster mode. It does not deploy Sentinel, a proxy, or
|
||||||
|
an active-active database. Every hash slot has exactly one writable primary;
|
||||||
|
write capacity scales because different slot ranges belong to different
|
||||||
|
primaries.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Architecture](#architecture)
|
||||||
|
- [Requirements](#requirements)
|
||||||
|
- [Quick start](#quick-start)
|
||||||
|
- [Connecting clients](#connecting-clients)
|
||||||
|
- [Authentication](#authentication)
|
||||||
|
- [Persistence](#persistence)
|
||||||
|
- [External access](#external-access)
|
||||||
|
- [RedisInsight](#redisinsight)
|
||||||
|
- [Metrics](#metrics)
|
||||||
|
- [Network policy and service mesh](#network-policy-and-service-mesh)
|
||||||
|
- [Scheduling and availability](#scheduling-and-availability)
|
||||||
|
- [Safe upgrades](#safe-upgrades)
|
||||||
|
- [Scaling and topology changes](#scaling-and-topology-changes)
|
||||||
|
- [Backup and restore](#backup-and-restore)
|
||||||
|
- [Argo CD](#argo-cd)
|
||||||
|
- [Configuration reference](#configuration-reference)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [Uninstalling](#uninstalling)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The default topology is three shards with one replica per shard:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Kubernetes worker A Kubernetes worker B Kubernetes worker C
|
||||||
|
------------------- ------------------- -------------------
|
||||||
|
shard 0 member 0 shard 1 member 0 shard 2 member 0
|
||||||
|
canonical primary canonical primary canonical primary
|
||||||
|
|
||||||
|
shard 2 member 1 shard 0 member 1 shard 1 member 1
|
||||||
|
replica replica replica
|
||||||
|
```
|
||||||
|
|
||||||
|
That produces six Redis instances:
|
||||||
|
|
||||||
|
```text
|
||||||
|
3 writable primaries + 3 readable/failover replicas = 6 Redis pods
|
||||||
|
```
|
||||||
|
|
||||||
|
Each persistent member is a separate one-replica StatefulSet with its own PVC.
|
||||||
|
This design allows Kubernetes to enforce all of the following:
|
||||||
|
|
||||||
|
- all canonical primaries occupy different `kubernetes.io/hostname` domains;
|
||||||
|
- the members of one shard never occupy the same node;
|
||||||
|
- each replica is crossed onto the node containing another shard's primary;
|
||||||
|
- member identities and Redis node IDs survive pod replacement.
|
||||||
|
|
||||||
|
At first boot, the shard-0 member-0 pod coordinates cluster creation. It waits
|
||||||
|
for every pristine member, introduces all nodes, divides all 16,384 hash slots
|
||||||
|
between the primaries, and attaches each replica to its intended primary. It
|
||||||
|
refuses to overwrite partially initialized cluster state.
|
||||||
|
|
||||||
|
Redis can promote a replica after a primary failure. A reconciliation loop later
|
||||||
|
returns the cluster to its canonical crossed layout after the failed node comes
|
||||||
|
back. Redis Cluster replication is asynchronous; a narrow failure or partition
|
||||||
|
window can still lose an acknowledged write.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Kubernetes 1.25 or newer.
|
||||||
|
- Helm 3.
|
||||||
|
- At least `cluster.shards` schedulable worker nodes with distinct
|
||||||
|
`kubernetes.io/hostname` labels. The default requires three workers.
|
||||||
|
- Enough CPU and memory on those workers for every Redis and exporter pod.
|
||||||
|
- A default StorageClass, or an explicit `persistence.storageClass`, when
|
||||||
|
persistence is enabled.
|
||||||
|
- Working pod DNS and unrestricted Redis client and cluster-bus traffic between
|
||||||
|
members.
|
||||||
|
|
||||||
|
Optional features have additional requirements:
|
||||||
|
|
||||||
|
- external access requires a controller that implements `type: LoadBalancer`
|
||||||
|
Services and populates `.status.loadBalancer.ingress`;
|
||||||
|
- `metrics.serviceMonitor.enabled` requires the Prometheus Operator CRDs;
|
||||||
|
- RedisInsight Istio ingress requires the Istio networking CRDs;
|
||||||
|
- automatic RedisInsight certificate creation requires cert-manager.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
Clone the repository and install an internal-only cluster:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://git.mulas.me/corrado/redis-cluster.git
|
||||||
|
cd redis-cluster
|
||||||
|
|
||||||
|
helm install redis . \
|
||||||
|
--namespace redis \
|
||||||
|
--create-namespace
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for all six default members:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl get pods -n redis -w
|
||||||
|
```
|
||||||
|
|
||||||
|
Check cluster health and slot coverage:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl exec -n redis redis-redis-cluster-s0-n0-0 -c redis -- \
|
||||||
|
redis-cli cluster info
|
||||||
|
|
||||||
|
kubectl exec -n redis redis-redis-cluster-s0-n0-0 -c redis -- \
|
||||||
|
redis-cli --cluster check 127.0.0.1:6379
|
||||||
|
```
|
||||||
|
|
||||||
|
`cluster_state:ok`, `cluster_slots_ok:16384`, and zero failed slots indicate a
|
||||||
|
healthy cluster.
|
||||||
|
|
||||||
|
### Recommended production values
|
||||||
|
|
||||||
|
Create a values file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
existingSecret: redis-auth
|
||||||
|
existingSecretKey: redis-password
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
storageClass: fast-storage
|
||||||
|
size: 100Gi
|
||||||
|
|
||||||
|
redis:
|
||||||
|
maxmemory: 80gb
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 84Gi
|
||||||
|
limits:
|
||||||
|
memory: 88Gi
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
serviceMonitor:
|
||||||
|
enabled: true
|
||||||
|
labels:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
|
||||||
|
networkPolicy:
|
||||||
|
enabled: true
|
||||||
|
allowExternal: false
|
||||||
|
extraIngress:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: applications
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the password Secret without committing the password:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl create namespace redis
|
||||||
|
kubectl create secret generic redis-auth \
|
||||||
|
--namespace redis \
|
||||||
|
--from-literal=redis-password="$(openssl rand -base64 32)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Install:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
helm upgrade --install redis . \
|
||||||
|
--namespace redis \
|
||||||
|
--create-namespace \
|
||||||
|
--values production-values.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep `redis.maxmemory` below the container memory limit. Redis needs headroom
|
||||||
|
for replication buffers, client buffers, fork/copy-on-write activity, AOF
|
||||||
|
rewrites, allocator overhead, and the operating process.
|
||||||
|
|
||||||
|
## Connecting clients
|
||||||
|
|
||||||
|
Redis Cluster requires a cluster-aware client. A standalone or Sentinel client
|
||||||
|
will not correctly follow `MOVED` and `ASK` redirects.
|
||||||
|
|
||||||
|
The internal balanced bootstrap endpoint is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<release>-redis-cluster.<namespace>.svc:<redis.port>
|
||||||
|
```
|
||||||
|
|
||||||
|
For the example release:
|
||||||
|
|
||||||
|
```text
|
||||||
|
redis-redis-cluster.redis.svc:6379
|
||||||
|
```
|
||||||
|
|
||||||
|
The bootstrap Service only provides an initial node. The client downloads the
|
||||||
|
slot map and then connects directly to the member endpoints advertised by
|
||||||
|
Redis.
|
||||||
|
|
||||||
|
### redis-cli
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl run redis-cli --rm -it \
|
||||||
|
--namespace redis \
|
||||||
|
--image=redis:7.4-alpine \
|
||||||
|
-- redis-cli -c -h redis-redis-cluster -p 6379
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `-a "$REDIS_PASSWORD"` when authentication is enabled.
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from redis.cluster import RedisCluster
|
||||||
|
|
||||||
|
client = RedisCluster(
|
||||||
|
host="redis-redis-cluster.redis.svc",
|
||||||
|
port=6379,
|
||||||
|
password="replace-me",
|
||||||
|
decode_responses=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
client.set("hello", "world")
|
||||||
|
print(client.get("hello"))
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `read_from_replicas=True` only if stale replica reads are acceptable.
|
||||||
|
|
||||||
|
### Multi-key operations
|
||||||
|
|
||||||
|
Transactions, Lua scripts, and multi-key commands can only span keys in one hash
|
||||||
|
slot. Use a shared hash tag when keys must be colocated:
|
||||||
|
|
||||||
|
```text
|
||||||
|
cart:{customer-42}
|
||||||
|
cart-items:{customer-42}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Authentication is disabled by default so the chart can be evaluated without
|
||||||
|
secret bootstrapping. Enable it for production.
|
||||||
|
|
||||||
|
### Explicit password
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
password: replace-me
|
||||||
|
```
|
||||||
|
|
||||||
|
This places the password in Helm release data and is not recommended for Git.
|
||||||
|
|
||||||
|
### Existing Secret
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
existingSecret: redis-auth
|
||||||
|
existingSecretKey: redis-password
|
||||||
|
```
|
||||||
|
|
||||||
|
The Secret must exist in the release namespace:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: redis-auth
|
||||||
|
namespace: redis
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
redis-password: CHANGE-ME
|
||||||
|
```
|
||||||
|
|
||||||
|
Use SOPS, Sealed Secrets, External Secrets, or another secret manager rather
|
||||||
|
than committing the cleartext example.
|
||||||
|
|
||||||
|
### Generated password
|
||||||
|
|
||||||
|
When `auth.enabled=true` and neither `password` nor `existingSecret` is set, the
|
||||||
|
chart generates a password. A normal Helm upgrade uses `lookup` to reuse the
|
||||||
|
existing Secret.
|
||||||
|
|
||||||
|
Retrieve it with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl get secret -n redis redis-redis-cluster \
|
||||||
|
-o jsonpath='{.data.redis-password}' | base64 -d
|
||||||
|
echo
|
||||||
|
```
|
||||||
|
|
||||||
|
Argo CD renders charts without live-cluster access, so generated passwords need
|
||||||
|
the `ignoreDifferences` treatment shown in
|
||||||
|
[`argocd/application.yaml`](argocd/application.yaml), or preferably an
|
||||||
|
externally managed Secret.
|
||||||
|
|
||||||
|
The chart configures Redis `requirepass` and `masterauth`. It does not currently
|
||||||
|
manage Redis ACL users or Redis TLS.
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
Persistence is enabled by default. Every Redis member receives an independent
|
||||||
|
PVC and writes an append-only file using `appendfsync everysec`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
storageClass: ""
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
size: 3Gi
|
||||||
|
```
|
||||||
|
|
||||||
|
An empty `storageClass` uses the cluster default. Any valid Kubernetes storage
|
||||||
|
quantity is accepted, including `500Mi`, `100Gi`, and `2Ti`. The chart imposes
|
||||||
|
no upper size limit. Actual capacity is controlled by the StorageClass,
|
||||||
|
provisioner, namespace quotas, and infrastructure.
|
||||||
|
|
||||||
|
`persistence.size` is per member, not total capacity. With three shards and one
|
||||||
|
replica, `100Gi` requests six 100 Gi volumes.
|
||||||
|
|
||||||
|
When persistence is disabled, Redis uses `emptyDir` and all data on a member is
|
||||||
|
lost when its pod is rescheduled.
|
||||||
|
|
||||||
|
### Expanding existing volumes
|
||||||
|
|
||||||
|
Increasing the value for a new installation is straightforward. Existing
|
||||||
|
StatefulSet `volumeClaimTemplates` are immutable, so changing
|
||||||
|
`persistence.size` alone may be rejected and does not reliably resize existing
|
||||||
|
claims.
|
||||||
|
|
||||||
|
To expand an existing deployment:
|
||||||
|
|
||||||
|
1. Confirm the StorageClass has `allowVolumeExpansion: true`.
|
||||||
|
2. Back up the cluster.
|
||||||
|
3. Patch each existing PVC to the new requested size.
|
||||||
|
4. Wait for the storage controller and filesystem resize to complete.
|
||||||
|
5. Keep `persistence.size` at the new value for future members.
|
||||||
|
|
||||||
|
Never shrink Redis PVCs in place.
|
||||||
|
|
||||||
|
## External access
|
||||||
|
|
||||||
|
External access is disabled by default because Redis Cluster cannot be exposed
|
||||||
|
correctly through a single generic TCP LoadBalancer.
|
||||||
|
|
||||||
|
When enabled, the chart creates:
|
||||||
|
|
||||||
|
- one balanced bootstrap LoadBalancer Service; and
|
||||||
|
- one LoadBalancer Service for every Redis member.
|
||||||
|
|
||||||
|
The default 3x1 topology therefore creates seven LoadBalancers. This can incur
|
||||||
|
cloud-provider cost and consume seven addresses.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
externalAccess:
|
||||||
|
enabled: true
|
||||||
|
port: 6379
|
||||||
|
loadBalancerClass: ""
|
||||||
|
externalTrafficPolicy: Cluster
|
||||||
|
loadBalancerSourceRanges:
|
||||||
|
- 203.0.113.0/24
|
||||||
|
```
|
||||||
|
|
||||||
|
The chart does not assume MetalLB or a particular cloud provider. It waits for
|
||||||
|
each member Service to publish either an IP or hostname under
|
||||||
|
`.status.loadBalancer.ingress`, then makes that member advertise the discovered
|
||||||
|
endpoint. All Services use the same Redis port; there is no fixed IP list and
|
||||||
|
no port fan-out.
|
||||||
|
|
||||||
|
Get the bootstrap endpoint:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl get service -n redis redis-redis-cluster-external \
|
||||||
|
-o jsonpath='{range .status.loadBalancer.ingress[*]}{.ip}{.hostname}{"\n"}{end}'
|
||||||
|
```
|
||||||
|
|
||||||
|
External clients need only that bootstrap endpoint in their startup
|
||||||
|
configuration, but they must be able to route to every per-member address
|
||||||
|
returned in the slot map. A client that can reach the bootstrap address but not
|
||||||
|
the member addresses will connect initially and then fail on redirected keys.
|
||||||
|
|
||||||
|
Keep the LoadBalancer Services instead of deleting and recreating them. If the
|
||||||
|
controller assigns a new member address, restart that member's one-pod
|
||||||
|
StatefulSet so its discovery init container records and advertises the new
|
||||||
|
endpoint.
|
||||||
|
|
||||||
|
External Redis traffic is unencrypted. Restrict
|
||||||
|
`loadBalancerSourceRanges`, use upstream firewalls/private routing, enable
|
||||||
|
authentication, or provide network-layer encryption. Do not expose an
|
||||||
|
unauthenticated cluster to the public Internet.
|
||||||
|
|
||||||
|
### Provider annotations
|
||||||
|
|
||||||
|
Bootstrap and member annotations are separate:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
externalAccess:
|
||||||
|
bootstrap:
|
||||||
|
annotations:
|
||||||
|
service.beta.kubernetes.io/example: bootstrap-value
|
||||||
|
memberServices:
|
||||||
|
annotations:
|
||||||
|
service.beta.kubernetes.io/example: member-value
|
||||||
|
```
|
||||||
|
|
||||||
|
Use these for provider-specific address pools, internal LoadBalancers, health
|
||||||
|
checks, or DNS controllers.
|
||||||
|
|
||||||
|
## RedisInsight
|
||||||
|
|
||||||
|
RedisInsight is optional and disabled by default:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
redisInsight:
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
It creates one Deployment, one ClusterIP Service, and optionally a dedicated
|
||||||
|
PVC. RedisInsight stores its SQLite state, saved connections, and logs under
|
||||||
|
`/data`.
|
||||||
|
|
||||||
|
### Automatic cluster connection
|
||||||
|
|
||||||
|
No IP address is required. With `redisInsight.connection.host=""`, the chart
|
||||||
|
preconfigures RedisInsight from the release-aware balanced bootstrap endpoint:
|
||||||
|
|
||||||
|
- with external access disabled, it uses the internal
|
||||||
|
`<release>-redis-cluster` Service DNS;
|
||||||
|
- with external access enabled, an init container reads the published IP or
|
||||||
|
hostname from the `<release>-redis-cluster-external` LoadBalancer Service and
|
||||||
|
injects that address before RedisInsight starts.
|
||||||
|
|
||||||
|
Kubernetes and the LoadBalancer controller therefore own the VIP lifecycle.
|
||||||
|
RedisInsight follows the correct release and namespace without a hard-coded
|
||||||
|
address. Port `0` automatically follows `redis.port` or
|
||||||
|
`externalAccess.port`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
redisInsight:
|
||||||
|
enabled: true
|
||||||
|
connection:
|
||||||
|
host: ""
|
||||||
|
port: 0
|
||||||
|
alias: redis-cluster
|
||||||
|
tls: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `host` and `port` when RedisInsight should inspect another cluster, or when
|
||||||
|
pods cannot hairpin through the external LoadBalancer endpoint. When chart
|
||||||
|
authentication is enabled, RedisInsight reads the same password Secret
|
||||||
|
automatically.
|
||||||
|
|
||||||
|
Port-forward the UI:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl port-forward -n redis \
|
||||||
|
service/redis-redis-cluster-insight 5540:80
|
||||||
|
```
|
||||||
|
|
||||||
|
Open <http://127.0.0.1:5540>.
|
||||||
|
|
||||||
|
### Istio HTTP ingress
|
||||||
|
|
||||||
|
The chart can create an Istio Gateway and VirtualService for RedisInsight:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
redisInsight:
|
||||||
|
enabled: true
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
host: redisinsight.example.com
|
||||||
|
gatewaySelector:
|
||||||
|
istio: ingressgateway
|
||||||
|
requestTimeout: 60s
|
||||||
|
tls:
|
||||||
|
enabled: false
|
||||||
|
```
|
||||||
|
|
||||||
|
The host is required when ingress is enabled. Hosting RedisInsight under a
|
||||||
|
rewritten path prefix is not supported; give it a host and route `/`.
|
||||||
|
|
||||||
|
### Istio TLS with an existing Secret
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
redisInsight:
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
host: redisinsight.example.com
|
||||||
|
tls:
|
||||||
|
enabled: true
|
||||||
|
credentialName: redisinsight-tls
|
||||||
|
certificate:
|
||||||
|
create: false
|
||||||
|
```
|
||||||
|
|
||||||
|
The credential Secret must be available to the selected Istio ingress gateway.
|
||||||
|
Exact Secret placement depends on the Istio installation.
|
||||||
|
|
||||||
|
### Istio TLS with cert-manager
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
redisInsight:
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
host: redisinsight.example.com
|
||||||
|
tls:
|
||||||
|
enabled: true
|
||||||
|
credentialName: redisinsight-tls
|
||||||
|
certificate:
|
||||||
|
create: true
|
||||||
|
issuerName: letsencrypt-production
|
||||||
|
issuerKind: ClusterIssuer
|
||||||
|
secretNamespace: istio-system
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `secretNamespace` to the namespace from which the ingress gateway reads
|
||||||
|
credential Secrets. An empty value uses the Helm release namespace.
|
||||||
|
|
||||||
|
RedisInsight is an administrative interface and does not provide ingress
|
||||||
|
authentication. Put an identity-aware proxy or Istio authorization policy in
|
||||||
|
front of it and restrict network exposure.
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
The Redis exporter sidecar is enabled by default on every member:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metrics:
|
||||||
|
enabled: true
|
||||||
|
port: 9121
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable a ServiceMonitor when the Prometheus Operator CRDs are installed:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metrics:
|
||||||
|
serviceMonitor:
|
||||||
|
enabled: true
|
||||||
|
interval: 30s
|
||||||
|
scrapeTimeout: 10s
|
||||||
|
labels:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
```
|
||||||
|
|
||||||
|
Use labels selected by your Prometheus installation.
|
||||||
|
|
||||||
|
## Network policy and service mesh
|
||||||
|
|
||||||
|
### NetworkPolicy
|
||||||
|
|
||||||
|
NetworkPolicy creation is disabled by default because policy enforcement and
|
||||||
|
default-deny behavior vary between CNIs.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
networkPolicy:
|
||||||
|
enabled: true
|
||||||
|
allowExternal: false
|
||||||
|
extraIngress:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: applications
|
||||||
|
metricsFromNamespaces:
|
||||||
|
- monitoring
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated policy always allows Redis client, replication, migration, and
|
||||||
|
cluster-bus traffic between release pods. `allowExternal` refers to Redis client
|
||||||
|
ingress from outside the selected pods; it does not create a LoadBalancer.
|
||||||
|
|
||||||
|
### Istio sidecars
|
||||||
|
|
||||||
|
Redis pods disable Istio injection by default:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
istio:
|
||||||
|
injectSidecar: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Redis Cluster uses direct node-to-node client and cluster-bus connections.
|
||||||
|
Namespace-wide strict mTLS can break this traffic. Either keep the Redis
|
||||||
|
workload outside strict mesh enforcement, create a scoped PERMISSIVE
|
||||||
|
PeerAuthentication, or deliberately enable and validate sidecars before
|
||||||
|
production use.
|
||||||
|
|
||||||
|
## Scheduling and availability
|
||||||
|
|
||||||
|
The chart uses required pod affinity and anti-affinity, not preferences. It will
|
||||||
|
leave pods Pending rather than silently place a primary beside its own replica.
|
||||||
|
|
||||||
|
For the default topology:
|
||||||
|
|
||||||
|
- three workers are required;
|
||||||
|
- losing one worker removes one primary and a replica of another shard;
|
||||||
|
- the lost primary's replica is on a surviving worker;
|
||||||
|
- two surviving primaries form the majority needed to authorize promotion.
|
||||||
|
|
||||||
|
Inspect placement:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl get pods -n redis \
|
||||||
|
-L redis-cluster.shard,redis-cluster.member \
|
||||||
|
-o wide
|
||||||
|
```
|
||||||
|
|
||||||
|
The two members of each shard must have different `NODE` values. All member-0
|
||||||
|
pods must also occupy different nodes.
|
||||||
|
|
||||||
|
The global PodDisruptionBudget defaults to `maxUnavailable: 1`. It limits
|
||||||
|
cooperating voluntary disruptions but cannot prevent node crashes or an
|
||||||
|
administrator from deleting multiple pods.
|
||||||
|
|
||||||
|
### Failure behavior
|
||||||
|
|
||||||
|
- Failure detection begins after approximately
|
||||||
|
`cluster.nodeTimeoutMilliseconds`.
|
||||||
|
- A healthy cross-node replica can be elected primary.
|
||||||
|
- Cluster-aware clients refresh the slot map and reconnect.
|
||||||
|
- Losing both members of one shard loses that slot range.
|
||||||
|
- With `cluster.requireFullCoverage=true`, loss of any slot range makes the
|
||||||
|
cluster unavailable until coverage returns.
|
||||||
|
- Automatic failover requires a majority of current primaries to communicate.
|
||||||
|
|
||||||
|
## Safe upgrades
|
||||||
|
|
||||||
|
Every member StatefulSet uses `updateStrategy: OnDelete`. A Helm or Argo update
|
||||||
|
changes the pod template but deliberately does not restart all independent
|
||||||
|
members at once.
|
||||||
|
|
||||||
|
Restart exactly one member at a time:
|
||||||
|
|
||||||
|
1. Inspect `CLUSTER NODES` and choose a current replica first.
|
||||||
|
2. Delete that one pod.
|
||||||
|
3. Wait for the replacement to become Ready.
|
||||||
|
4. Confirm `cluster_state:ok`.
|
||||||
|
5. Continue with the next replica, then primaries one by one.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl exec -n redis redis-redis-cluster-s0-n0-0 -c redis -- \
|
||||||
|
redis-cli cluster nodes
|
||||||
|
|
||||||
|
kubectl delete pod -n redis redis-redis-cluster-s0-n1-0
|
||||||
|
|
||||||
|
kubectl wait -n redis \
|
||||||
|
--for=condition=Ready \
|
||||||
|
pod/redis-redis-cluster-s0-n1-0 \
|
||||||
|
--timeout=300s
|
||||||
|
|
||||||
|
kubectl exec -n redis redis-redis-cluster-s0-n0-0 -c redis -- \
|
||||||
|
redis-cli cluster info
|
||||||
|
```
|
||||||
|
|
||||||
|
The preStop hook asks a healthy replica to take over before an intentional
|
||||||
|
primary restart. Never delete both members of one shard together.
|
||||||
|
|
||||||
|
## Scaling and topology changes
|
||||||
|
|
||||||
|
`cluster.shards` and `cluster.replicasPerShard` define the first-boot topology.
|
||||||
|
Changing them in Helm on a populated cluster does not reshard existing data and
|
||||||
|
is not a supported scaling procedure.
|
||||||
|
|
||||||
|
Adding a shard requires an explicit operational workflow:
|
||||||
|
|
||||||
|
1. create and persist the new members;
|
||||||
|
2. join them with `CLUSTER MEET`;
|
||||||
|
3. assign replicas;
|
||||||
|
4. move slots with `redis-cli --cluster reshard`;
|
||||||
|
5. verify slot coverage and failure placement.
|
||||||
|
|
||||||
|
Removing a shard requires moving all of its slots away before removing its
|
||||||
|
nodes. Adding replicas requires joining the nodes and issuing
|
||||||
|
`CLUSTER REPLICATE`. Back up first.
|
||||||
|
|
||||||
|
The chart enforces at least three shards, at least one replica per shard, and
|
||||||
|
fewer replicas per shard than shards so the crossed placement remains valid.
|
||||||
|
|
||||||
|
## Backup and restore
|
||||||
|
|
||||||
|
This chart does not schedule backups. Redis Cluster stores different slot ranges
|
||||||
|
on different primaries, so a backup strategy must cover every shard.
|
||||||
|
|
||||||
|
Common approaches include:
|
||||||
|
|
||||||
|
- crash-consistent snapshots of all member volumes;
|
||||||
|
- coordinated AOF/RDB copies from one healthy member of every shard;
|
||||||
|
- a Redis-aware backup product that understands cluster topology.
|
||||||
|
|
||||||
|
Test restore procedures. Restoring only one shard or one arbitrary member does
|
||||||
|
not reconstruct the complete keyspace. Keep backups outside the cluster and
|
||||||
|
verify both data and all 16,384 slots after recovery.
|
||||||
|
|
||||||
|
## Argo CD
|
||||||
|
|
||||||
|
[`argocd/application.yaml`](argocd/application.yaml) is a portable example using
|
||||||
|
the public repository. Apply it once:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl apply -f argocd/application.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
The file is outside `templates/`, so it is not part of the Helm release. If you
|
||||||
|
edit its inline `valuesObject`, a normal sync of the Redis Application cannot
|
||||||
|
update the Application's own spec. Reapply the file, manage it from an
|
||||||
|
app-of-apps, or use an ApplicationSet:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl apply -f argocd/application.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
For authentication, prefer an externally managed Secret. If the chart generates
|
||||||
|
the password, retain `RespectIgnoreDifferences=true` and the Secret
|
||||||
|
`ignoreDifferences` rule from the example.
|
||||||
|
|
||||||
|
Argo applies new StatefulSet templates, but `OnDelete` intentionally prevents
|
||||||
|
automatic Redis pod rolls. Perform the one-member-at-a-time procedure after
|
||||||
|
reviewing each change. Additional authentication, external access, and Istio
|
||||||
|
examples are in [`argocd/README.md`](argocd/README.md).
|
||||||
|
|
||||||
|
## Configuration reference
|
||||||
|
|
||||||
|
The authoritative defaults are in [`values.yaml`](values.yaml), and
|
||||||
|
[`values.schema.json`](values.schema.json) validates supported values.
|
||||||
|
|
||||||
|
### Release and image
|
||||||
|
|
||||||
|
| Value | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `nameOverride` | `""` | Override the chart name portion of resource names. |
|
||||||
|
| `fullnameOverride` | `""` | Override the complete release resource prefix; maximum 53 characters. |
|
||||||
|
| `image.repository` | `redis` | Redis image repository. |
|
||||||
|
| `image.tag` | `7.4-alpine` | Redis image tag. |
|
||||||
|
| `image.pullPolicy` | `IfNotPresent` | Redis image pull policy. |
|
||||||
|
| `imagePullSecrets` | `[]` | Pod image pull Secret references. |
|
||||||
|
|
||||||
|
### Cluster
|
||||||
|
|
||||||
|
| Value | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `cluster.shards` | `3` | Independently writable primaries and slot ranges. |
|
||||||
|
| `cluster.replicasPerShard` | `1` | Readable failover replicas per shard. |
|
||||||
|
| `cluster.busPort` | `16379` | Redis node-to-node cluster bus port. |
|
||||||
|
| `cluster.nodeTimeoutMilliseconds` | `10000` | Failure-detection timeout. |
|
||||||
|
| `cluster.requireFullCoverage` | `true` | Stop serving when any slot is unavailable. |
|
||||||
|
| `cluster.allowReadsWhenDown` | `false` | Permit reads while cluster state is down. |
|
||||||
|
| `cluster.migrationBarrier` | `1` | Minimum healthy replicas retained during replica migration. |
|
||||||
|
|
||||||
|
### Redis and authentication
|
||||||
|
|
||||||
|
| Value | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `auth.enabled` | `false` | Enable password authentication. |
|
||||||
|
| `auth.password` | `""` | Explicit password; generated when empty and no Secret is supplied. |
|
||||||
|
| `auth.existingSecret` | `""` | Existing password Secret name. |
|
||||||
|
| `auth.existingSecretKey` | `redis-password` | Password key in the existing Secret. |
|
||||||
|
| `redis.port` | `6379` | Redis client port. |
|
||||||
|
| `redis.maxmemory` | `384mb` | Redis memory ceiling. Empty disables the explicit ceiling. |
|
||||||
|
| `redis.maxmemoryPolicy` | `noeviction` | Redis eviction policy. |
|
||||||
|
| `redis.extraConfig` | `""` | Additional lines appended to `redis.conf`. |
|
||||||
|
| `redis.resources` | see values | Redis container requests and limits. |
|
||||||
|
|
||||||
|
Do not use `redis.extraConfig` to override cluster identity, announce endpoints,
|
||||||
|
data directory, authentication, or other settings managed by the chart.
|
||||||
|
|
||||||
|
### Probes
|
||||||
|
|
||||||
|
| Value | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `probes.redis.startup.initialDelaySeconds` | `10` | Delay before startup checks. |
|
||||||
|
| `probes.redis.startup.periodSeconds` | `5` | Startup check interval. |
|
||||||
|
| `probes.redis.startup.timeoutSeconds` | `4` | Startup check timeout. |
|
||||||
|
| `probes.redis.startup.failureThreshold` | `180` | Failures allowed before restart. |
|
||||||
|
| `probes.redis.liveness.periodSeconds` | `15` | Liveness check interval. |
|
||||||
|
| `probes.redis.liveness.timeoutSeconds` | `8` | Liveness check timeout. |
|
||||||
|
| `probes.redis.liveness.failureThreshold` | `10` | Liveness failures before restart. |
|
||||||
|
| `probes.redis.readiness.periodSeconds` | `5` | Readiness check interval. |
|
||||||
|
| `probes.redis.readiness.timeoutSeconds` | `4` | Readiness check timeout. |
|
||||||
|
| `probes.redis.readiness.failureThreshold` | `3` | Readiness failures before removal from Services. |
|
||||||
|
|
||||||
|
### Persistence
|
||||||
|
|
||||||
|
| Value | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `persistence.enabled` | `true` | Create one PVC per Redis member. |
|
||||||
|
| `persistence.storageClass` | `""` | StorageClass; empty uses the cluster default. |
|
||||||
|
| `persistence.accessModes` | `[ReadWriteOnce]` | PVC access modes. |
|
||||||
|
| `persistence.size` | `3Gi` | Capacity per member; no chart-side maximum. |
|
||||||
|
| `persistence.annotations` | `{}` | PVC template annotations. |
|
||||||
|
|
||||||
|
### External access
|
||||||
|
|
||||||
|
| Value | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `externalAccess.enabled` | `false` | Create bootstrap and per-member LoadBalancers. |
|
||||||
|
| `externalAccess.port` | `6379` | External Redis port on every Service. |
|
||||||
|
| `externalAccess.bootstrap.annotations` | `{}` | Bootstrap Service annotations. |
|
||||||
|
| `externalAccess.memberServices.annotations` | `{}` | Per-member Service annotations. |
|
||||||
|
| `externalAccess.loadBalancerClass` | `""` | Optional LoadBalancer implementation class. |
|
||||||
|
| `externalAccess.externalTrafficPolicy` | `Cluster` | Kubernetes external traffic policy. |
|
||||||
|
| `externalAccess.loadBalancerSourceRanges` | `[]` | Allowed client CIDRs. |
|
||||||
|
| `externalAccess.allocateLoadBalancerNodePorts` | `true` | Allocate backing NodePorts. |
|
||||||
|
| `externalAccess.endpointDiscovery.*` | see values | Image/resources for endpoint discovery. |
|
||||||
|
|
||||||
|
### RedisInsight
|
||||||
|
|
||||||
|
| Value | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `redisInsight.enabled` | `false` | Deploy RedisInsight. |
|
||||||
|
| `redisInsight.image.repository` | `redis/redisinsight` | RedisInsight image repository. |
|
||||||
|
| `redisInsight.image.tag` | `3.8.0` | RedisInsight image tag. |
|
||||||
|
| `redisInsight.image.pullPolicy` | `IfNotPresent` | RedisInsight image pull policy. |
|
||||||
|
| `redisInsight.port` | `5540` | RedisInsight container port. |
|
||||||
|
| `redisInsight.connection.host` | `""` | Empty discovers the balanced release bootstrap endpoint. |
|
||||||
|
| `redisInsight.connection.port` | `0` | Zero follows the selected Redis Service port. |
|
||||||
|
| `redisInsight.connection.alias` | `redis-cluster` | Display name in RedisInsight. |
|
||||||
|
| `redisInsight.connection.tls` | `false` | Enable TLS for the Redis connection. |
|
||||||
|
| `redisInsight.persistence.enabled` | `true` | Persist RedisInsight state. |
|
||||||
|
| `redisInsight.persistence.storageClass` | `""` | StorageClass; empty uses default. |
|
||||||
|
| `redisInsight.persistence.accessModes` | `[ReadWriteOnce]` | RedisInsight PVC access modes. |
|
||||||
|
| `redisInsight.persistence.size` | `1Gi` | RedisInsight PVC size; no chart-side maximum. |
|
||||||
|
| `redisInsight.persistence.annotations` | `{}` | RedisInsight PVC annotations. |
|
||||||
|
| `redisInsight.resources` | see values | RedisInsight requests and limits. |
|
||||||
|
| `redisInsight.podAnnotations` | `{}` | RedisInsight pod annotations. |
|
||||||
|
| `redisInsight.nodeSelector` | `{}` | RedisInsight node selector. |
|
||||||
|
| `redisInsight.tolerations` | `[]` | RedisInsight pod tolerations. |
|
||||||
|
| `redisInsight.ingress.enabled` | `false` | Create Istio Gateway and VirtualService. |
|
||||||
|
| `redisInsight.ingress.host` | `""` | Required hostname when ingress is enabled. |
|
||||||
|
| `redisInsight.ingress.gatewaySelector` | `{istio: ingressgateway}` | Select the Istio gateway workload. |
|
||||||
|
| `redisInsight.ingress.requestTimeout` | `60s` | Istio route timeout. |
|
||||||
|
| `redisInsight.ingress.tls.enabled` | `false` | Serve HTTPS on the Gateway. |
|
||||||
|
| `redisInsight.ingress.tls.credentialName` | `""` | Istio TLS credential Secret. |
|
||||||
|
| `redisInsight.ingress.tls.certificate.create` | `false` | Create the Secret using cert-manager. |
|
||||||
|
| `redisInsight.ingress.tls.certificate.*` | see values | Issuer and Secret namespace settings. |
|
||||||
|
|
||||||
|
### Metrics, policy, and pod settings
|
||||||
|
|
||||||
|
| Value | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `metrics.enabled` | `true` | Add redis_exporter to every member pod. |
|
||||||
|
| `metrics.image.repository` | `ghcr.io/oliver006/redis_exporter` | Exporter image repository. |
|
||||||
|
| `metrics.image.tag` | `v1.87.0-alpine` | Exporter image tag. |
|
||||||
|
| `metrics.image.pullPolicy` | `IfNotPresent` | Exporter pull policy. |
|
||||||
|
| `metrics.port` | `9121` | Exporter port. |
|
||||||
|
| `metrics.resources` | see values | Exporter requests and limits. |
|
||||||
|
| `metrics.serviceMonitor.enabled` | `false` | Create a Prometheus Operator ServiceMonitor. |
|
||||||
|
| `metrics.serviceMonitor.interval` | `30s` | Prometheus scrape interval. |
|
||||||
|
| `metrics.serviceMonitor.scrapeTimeout` | `10s` | Prometheus scrape timeout. |
|
||||||
|
| `metrics.serviceMonitor.labels` | `{}` | Labels used by Prometheus to select the ServiceMonitor. |
|
||||||
|
| `networkPolicy.enabled` | `false` | Create an ingress NetworkPolicy. |
|
||||||
|
| `networkPolicy.allowExternal` | `true` | Allow Redis client ingress from all sources. |
|
||||||
|
| `networkPolicy.extraIngress` | `[]` | Allowed peers when unrestricted ingress is disabled. |
|
||||||
|
| `networkPolicy.metricsFromNamespaces` | `[]` | Namespaces allowed to scrape metrics. |
|
||||||
|
| `podDisruptionBudget.enabled` | `true` | Create a global Redis PDB. |
|
||||||
|
| `podDisruptionBudget.maxUnavailable` | `1` | Maximum voluntary unavailable Redis pods. |
|
||||||
|
| `serviceAccount.create` | `true` | Create the endpoint-discovery ServiceAccount. |
|
||||||
|
| `serviceAccount.name` | `""` | Existing or generated ServiceAccount name. |
|
||||||
|
| `serviceAccount.annotations` | `{}` | ServiceAccount annotations. |
|
||||||
|
| `nodeSelector` | `{}` | Redis pod node selector. |
|
||||||
|
| `tolerations` | `[]` | Redis pod tolerations. |
|
||||||
|
| `topologySpreadConstraints` | `[]` | Additional spread constraints. |
|
||||||
|
| `priorityClassName` | `""` | Redis pod priority class. |
|
||||||
|
| `podAnnotations` / `podLabels` | `{}` | Additional Redis pod metadata. |
|
||||||
|
| `podSecurityContext` | see values | Redis pod-level security context. |
|
||||||
|
| `containerSecurityContext` | see values | Redis/exporter container security context. |
|
||||||
|
| `istio.injectSidecar` | `false` | Enable sidecars on Redis member pods. |
|
||||||
|
| `terminationGracePeriodSeconds` | `30` | Pod shutdown grace period. |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Pods remain Pending
|
||||||
|
|
||||||
|
Inspect scheduling events:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl describe pod -n redis <pending-pod>
|
||||||
|
```
|
||||||
|
|
||||||
|
Common causes are:
|
||||||
|
|
||||||
|
- fewer schedulable workers than `cluster.shards`;
|
||||||
|
- insufficient CPU or memory;
|
||||||
|
- node taints without matching tolerations;
|
||||||
|
- node selectors excluding required workers;
|
||||||
|
- existing pods violating the required crossed affinity layout;
|
||||||
|
- unbound PVCs or a missing default StorageClass.
|
||||||
|
|
||||||
|
The scheduler message `didn't match pod affinity/anti-affinity rules` means the
|
||||||
|
availability invariant cannot currently be satisfied. Do not weaken it unless
|
||||||
|
you accept a primary and its replica sharing a failure domain.
|
||||||
|
|
||||||
|
### External-access pods wait in init
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl logs -n redis <pod> -c discover-external-endpoint
|
||||||
|
kubectl get services -n redis
|
||||||
|
```
|
||||||
|
|
||||||
|
The LoadBalancer controller must populate an IP or hostname for every member
|
||||||
|
Service. Check controller events, address-pool capacity, provider annotations,
|
||||||
|
quota, and `loadBalancerClass`.
|
||||||
|
|
||||||
|
### `cluster_state:fail`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl exec -n redis redis-redis-cluster-s0-n0-0 -c redis -- \
|
||||||
|
redis-cli cluster nodes
|
||||||
|
|
||||||
|
kubectl exec -n redis redis-redis-cluster-s0-n0-0 -c redis -- \
|
||||||
|
redis-cli cluster info
|
||||||
|
```
|
||||||
|
|
||||||
|
Check for missing members, failed slot ranges, DNS failures, blocked port 16379,
|
||||||
|
or a member that cannot read its PVC. Readiness intentionally remains false
|
||||||
|
until the complete cluster is healthy; liveness does not restart Redis merely
|
||||||
|
for `CLUSTERDOWN`.
|
||||||
|
|
||||||
|
### Authentication errors
|
||||||
|
|
||||||
|
Confirm the client is using the current Secret and that all pods reference the
|
||||||
|
same password:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl get secret -n redis redis-redis-cluster \
|
||||||
|
-o jsonpath='{.data.redis-password}' | base64 -d
|
||||||
|
echo
|
||||||
|
```
|
||||||
|
|
||||||
|
When using an existing Secret, verify its name/key match the Helm values. Restart
|
||||||
|
members one at a time if a password Secret was deliberately changed.
|
||||||
|
|
||||||
|
### External client connects, then fails
|
||||||
|
|
||||||
|
This almost always means the bootstrap address is reachable but the member
|
||||||
|
addresses in `CLUSTER SLOTS`/`CLUSTER SHARDS` are not. Route and permit every
|
||||||
|
per-member LoadBalancer endpoint.
|
||||||
|
|
||||||
|
### RedisInsight is not rendered by Argo CD
|
||||||
|
|
||||||
|
Confirm the live Application contains `redisInsight.enabled: true`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl get application -n argocd redis-cluster -o yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Changing `argocd/application.yaml` in this repository does not update the live
|
||||||
|
Application during its own Helm sync because the file is outside `templates/`.
|
||||||
|
Reapply it or manage it from an app-of-apps.
|
||||||
|
|
||||||
|
### Inspect rendered manifests
|
||||||
|
|
||||||
|
```sh
|
||||||
|
helm lint .
|
||||||
|
helm template redis . --namespace redis --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uninstalling
|
||||||
|
|
||||||
|
Back up first, then remove the release:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
helm uninstall redis --namespace redis
|
||||||
|
```
|
||||||
|
|
||||||
|
PVCs created from StatefulSet volume claim templates are normally retained when
|
||||||
|
the StatefulSets are deleted. Verify them before deleting anything:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl get pvc -n redis
|
||||||
|
```
|
||||||
|
|
||||||
|
The directly managed RedisInsight PVC may be removed with the Helm release,
|
||||||
|
depending on Helm and storage-policy behavior. Preserve or snapshot it if its UI
|
||||||
|
state matters.
|
||||||
|
|
||||||
|
Delete retained Redis data PVCs only when data destruction is intentional.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is distributed under the BSD 3-Clause License. See
|
||||||
|
[`LICENSE`](LICENSE).
|
||||||
|
|||||||
84
argocd/README.md
Normal file
84
argocd/README.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Argo CD examples
|
||||||
|
|
||||||
|
This directory is excluded from the packaged Helm chart. Its files are examples
|
||||||
|
for managing the chart from Argo CD.
|
||||||
|
|
||||||
|
## Application
|
||||||
|
|
||||||
|
[`application.yaml`](application.yaml) deploys:
|
||||||
|
|
||||||
|
- three Redis shards;
|
||||||
|
- one replica per shard;
|
||||||
|
- authenticated Redis with a chart-generated initial password;
|
||||||
|
- persistent volumes using the cluster's default StorageClass;
|
||||||
|
- internal-only Redis Services;
|
||||||
|
- RedisInsight connected automatically to the balanced bootstrap endpoint.
|
||||||
|
|
||||||
|
Apply it once:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl apply -f argocd/application.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit the destination namespace, target revision, and inline `valuesObject` as
|
||||||
|
needed. Because this file is not a Helm template, changing it in the Redis
|
||||||
|
Application's own repository does not change the live Application spec during a
|
||||||
|
normal sync. Reapply it, or manage it from an app-of-apps/ApplicationSet.
|
||||||
|
|
||||||
|
## Password Secret
|
||||||
|
|
||||||
|
For production GitOps, use an externally managed Secret and set:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
existingSecret: redis-auth
|
||||||
|
existingSecretKey: redis-password
|
||||||
|
```
|
||||||
|
|
||||||
|
[`redis-auth-secret.example.yaml`](redis-auth-secret.example.yaml) documents the
|
||||||
|
required Secret shape. Do not commit an actual cleartext password.
|
||||||
|
|
||||||
|
If the chart generates the password, retain
|
||||||
|
`RespectIgnoreDifferences=true` and the Secret ignore rule from
|
||||||
|
`application.yaml`; Argo's Helm renderer cannot use `lookup` to read the live
|
||||||
|
Secret while comparing desired state.
|
||||||
|
|
||||||
|
## Enabling external access
|
||||||
|
|
||||||
|
Add this to `valuesObject`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
externalAccess:
|
||||||
|
enabled: true
|
||||||
|
loadBalancerClass: ""
|
||||||
|
loadBalancerSourceRanges:
|
||||||
|
- 203.0.113.0/24
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates one balanced bootstrap LoadBalancer and one LoadBalancer per Redis
|
||||||
|
member. Every advertised member address must be routable from external clients.
|
||||||
|
|
||||||
|
## Exposing RedisInsight with Istio
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
redisInsight:
|
||||||
|
enabled: true
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
host: redisinsight.example.com
|
||||||
|
tls:
|
||||||
|
enabled: true
|
||||||
|
credentialName: redisinsight-tls
|
||||||
|
certificate:
|
||||||
|
create: true
|
||||||
|
issuerName: letsencrypt-production
|
||||||
|
issuerKind: ClusterIssuer
|
||||||
|
secretNamespace: istio-system
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the hostname, issuer, gateway selector, and Secret namespace with values
|
||||||
|
from your environment. RedisInsight has no built-in ingress authentication; add
|
||||||
|
an identity-aware proxy or Istio authorization policy before exposing it.
|
||||||
77
argocd/application.yaml
Normal file
77
argocd/application.yaml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Public Argo CD example. Adjust the destination namespace, revision and values
|
||||||
|
# for your environment, then apply this file once (or manage it from an
|
||||||
|
# app-of-apps). Files under argocd/ are examples and are not rendered by Helm.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: redis-cluster
|
||||||
|
namespace: argocd
|
||||||
|
finalizers:
|
||||||
|
- resources-finalizer.argocd.argoproj.io
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: https://git.mulas.me/corrado/redis-cluster.git
|
||||||
|
targetRevision: master
|
||||||
|
path: .
|
||||||
|
helm:
|
||||||
|
releaseName: redis
|
||||||
|
valuesObject:
|
||||||
|
cluster:
|
||||||
|
shards: 3
|
||||||
|
replicasPerShard: 1
|
||||||
|
|
||||||
|
# Production deployments should authenticate even when Redis is only
|
||||||
|
# reachable inside the cluster. This example lets the chart create the
|
||||||
|
# initial password Secret; see the ignore rule below.
|
||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
# Empty selects the cluster's default StorageClass.
|
||||||
|
storageClass: ""
|
||||||
|
# This is per Redis member. The chart imposes no maximum.
|
||||||
|
size: 10Gi
|
||||||
|
|
||||||
|
# Disabled by default because this creates one LoadBalancer per member
|
||||||
|
# plus one balanced bootstrap LoadBalancer.
|
||||||
|
externalAccess:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
serviceMonitor:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# RedisInsight automatically discovers the release's balanced
|
||||||
|
# bootstrap endpoint. No IP address is required.
|
||||||
|
redisInsight:
|
||||||
|
enabled: true
|
||||||
|
persistence:
|
||||||
|
storageClass: ""
|
||||||
|
size: 2Gi
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: redis
|
||||||
|
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
- RespectIgnoreDifferences=true
|
||||||
|
|
||||||
|
# Argo CD renders Helm without API access, so Helm's lookup cannot recover a
|
||||||
|
# previously generated password during comparison. Ignore the generated
|
||||||
|
# Secret data to keep the first password stable. Prefer an existing Secret
|
||||||
|
# managed by SOPS, Sealed Secrets or External Secrets for serious GitOps use.
|
||||||
|
ignoreDifferences:
|
||||||
|
- group: ""
|
||||||
|
kind: Secret
|
||||||
|
name: redis-redis-cluster
|
||||||
|
namespace: redis
|
||||||
|
jsonPointers:
|
||||||
|
- /data/redis-password
|
||||||
19
argocd/redis-auth-secret.example.yaml
Normal file
19
argocd/redis-auth-secret.example.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# OPTIONAL — the clean GitOps way to handle the password: create the secret
|
||||||
|
# out-of-band (or via SealedSecrets/SOPS/ExternalSecrets), then set
|
||||||
|
# auth.existingSecret: redis-auth in the Application's valuesObject.
|
||||||
|
#
|
||||||
|
# Do NOT commit a real password to git. Either apply this once by hand with a
|
||||||
|
# real value:
|
||||||
|
#
|
||||||
|
# kubectl create secret generic redis-auth -n redis \
|
||||||
|
# --from-literal=redis-password="$(openssl rand -base64 24)"
|
||||||
|
#
|
||||||
|
# ...or encrypt this file with your secret-management tool of choice.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: redis-auth
|
||||||
|
namespace: redis
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
redis-password: CHANGE-ME
|
||||||
63
templates/NOTES.txt
Normal file
63
templates/NOTES.txt
Normal 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
154
templates/_helpers.tpl
Normal 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
381
templates/configmap.yaml
Normal 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
|
||||||
37
templates/external-endpoint-rbac.yaml
Normal file
37
templates/external-endpoint-rbac.yaml
Normal 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 }}
|
||||||
51
templates/networkpolicy.yaml
Normal file
51
templates/networkpolicy.yaml
Normal 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
14
templates/pdb.yaml
Normal 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 }}
|
||||||
87
templates/redisinsight-istio.yaml
Normal file
87
templates/redisinsight-istio.yaml
Normal 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
220
templates/redisinsight.yaml
Normal 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
12
templates/secret.yaml
Normal 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 }}
|
||||||
75
templates/service-external.yaml
Normal file
75
templates/service-external.yaml
Normal 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 }}
|
||||||
22
templates/service-headless.yaml
Normal file
22
templates/service-headless.yaml
Normal 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
24
templates/service.yaml
Normal 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 }}
|
||||||
14
templates/serviceaccount.yaml
Normal file
14
templates/serviceaccount.yaml
Normal 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 }}
|
||||||
20
templates/servicemonitor.yaml
Normal file
20
templates/servicemonitor.yaml
Normal 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
311
templates/statefulset.yaml
Normal 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 }}
|
||||||
230
values.schema.json
Normal file
230
values.schema.json
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cluster": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["shards", "replicasPerShard", "busPort"],
|
||||||
|
"properties": {
|
||||||
|
"shards": { "type": "integer", "minimum": 3, "maximum": 99 },
|
||||||
|
"replicasPerShard": { "type": "integer", "minimum": 1, "maximum": 9 },
|
||||||
|
"busPort": { "type": "integer", "minimum": 1, "maximum": 65535 },
|
||||||
|
"nodeTimeoutMilliseconds": { "type": "integer", "minimum": 1000 },
|
||||||
|
"requireFullCoverage": { "type": "boolean" },
|
||||||
|
"allowReadsWhenDown": { "type": "boolean" },
|
||||||
|
"migrationBarrier": { "type": "integer", "minimum": 0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"password": { "type": "string" },
|
||||||
|
"existingSecret": { "type": "string" },
|
||||||
|
"existingSecretKey": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
|
||||||
|
"maxmemory": { "type": "string" },
|
||||||
|
"maxmemoryPolicy": {
|
||||||
|
"enum": [
|
||||||
|
"noeviction",
|
||||||
|
"allkeys-lru", "volatile-lru",
|
||||||
|
"allkeys-lfu", "volatile-lfu",
|
||||||
|
"allkeys-random", "volatile-random",
|
||||||
|
"volatile-ttl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extraConfig": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"probes": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"redis": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"startup": { "$ref": "#/definitions/probe" },
|
||||||
|
"liveness": { "$ref": "#/definitions/probe" },
|
||||||
|
"readiness": { "$ref": "#/definitions/probe" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"externalAccess": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
|
||||||
|
"bootstrap": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"annotations": { "type": "object" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"memberServices": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"annotations": { "type": "object" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loadBalancerClass": { "type": "string" },
|
||||||
|
"externalTrafficPolicy": { "enum": ["Cluster", "Local"] },
|
||||||
|
"loadBalancerSourceRanges": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string", "minLength": 1 }
|
||||||
|
},
|
||||||
|
"allocateLoadBalancerNodePorts": { "type": "boolean" },
|
||||||
|
"endpointDiscovery": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"image": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"repository": { "type": "string", "minLength": 1 },
|
||||||
|
"tag": { "type": "string", "minLength": 1 },
|
||||||
|
"pullPolicy": { "enum": ["Always", "IfNotPresent", "Never"] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": { "type": "object" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"port": { "type": "integer", "minimum": 1, "maximum": 65535 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"redisInsight": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["enabled", "image", "port", "connection", "persistence", "ingress"],
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"image": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["repository", "tag", "pullPolicy"],
|
||||||
|
"properties": {
|
||||||
|
"repository": { "type": "string", "minLength": 1 },
|
||||||
|
"tag": { "type": "string", "minLength": 1 },
|
||||||
|
"pullPolicy": { "enum": ["Always", "IfNotPresent", "Never"] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
|
||||||
|
"connection": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["host", "port", "alias", "tls"],
|
||||||
|
"properties": {
|
||||||
|
"host": { "type": "string" },
|
||||||
|
"port": { "type": "integer", "minimum": 0, "maximum": 65535 },
|
||||||
|
"alias": { "type": "string", "minLength": 1 },
|
||||||
|
"tls": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"persistence": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["enabled", "size"],
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"storageClass": { "type": "string" },
|
||||||
|
"accessModes": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": { "enum": ["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany", "ReadWriteOncePod"] }
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"pattern": "^([0-9]+([.][0-9]+)?|[.][0-9]+)([eE][+-]?[0-9]+|[num]|[kKMGTP]i?|Ei?)?$"
|
||||||
|
},
|
||||||
|
"annotations": { "type": "object" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": { "type": "object" },
|
||||||
|
"podAnnotations": { "type": "object" },
|
||||||
|
"nodeSelector": { "type": "object" },
|
||||||
|
"tolerations": { "type": "array" },
|
||||||
|
"ingress": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["enabled", "host", "gatewaySelector", "requestTimeout", "tls"],
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"host": { "type": "string" },
|
||||||
|
"gatewaySelector": { "type": "object", "minProperties": 1 },
|
||||||
|
"requestTimeout": { "type": "string", "minLength": 1 },
|
||||||
|
"tls": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["enabled", "credentialName", "certificate"],
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"credentialName": { "type": "string" },
|
||||||
|
"certificate": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["create", "issuerName", "issuerKind", "secretNamespace"],
|
||||||
|
"properties": {
|
||||||
|
"create": { "type": "boolean" },
|
||||||
|
"issuerName": { "type": "string" },
|
||||||
|
"issuerKind": { "enum": ["Issuer", "ClusterIssuer"] },
|
||||||
|
"secretNamespace": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"persistence": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"storageClass": { "type": "string" },
|
||||||
|
"size": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"pattern": "^([0-9]+([.][0-9]+)?|[.][0-9]+)([eE][+-]?[0-9]+|[num]|[kKMGTP]i?|Ei?)?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"networkPolicy": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"allowExternal": { "type": "boolean" },
|
||||||
|
"extraIngress": { "type": "array" },
|
||||||
|
"metricsFromNamespaces": { "type": "array", "items": { "type": "string" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"podDisruptionBudget": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"maxUnavailable": { "type": "integer", "minimum": 0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"istio": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"injectSidecar": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"terminationGracePeriodSeconds": { "type": "integer", "minimum": 0 }
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"probe": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["periodSeconds", "timeoutSeconds", "failureThreshold"],
|
||||||
|
"properties": {
|
||||||
|
"initialDelaySeconds": { "type": "integer", "minimum": 0 },
|
||||||
|
"periodSeconds": { "type": "integer", "minimum": 1 },
|
||||||
|
"timeoutSeconds": { "type": "integer", "minimum": 1 },
|
||||||
|
"failureThreshold": { "type": "integer", "minimum": 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
247
values.yaml
Normal file
247
values.yaml
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
# Native Redis Cluster topology. The member StatefulSets contain
|
||||||
|
# shards * (replicasPerShard + 1) pods. Changing either value after the cluster
|
||||||
|
# contains data requires an explicit Redis reshard/add-node operation; a Helm
|
||||||
|
# upgrade alone intentionally does not mutate the slot map.
|
||||||
|
cluster:
|
||||||
|
# -- Independently writable hash-slot primaries. Redis requires at least 3
|
||||||
|
# for automatic failover elections.
|
||||||
|
shards: 3
|
||||||
|
# -- Readable, failover-capable replicas attached to every shard primary.
|
||||||
|
replicasPerShard: 1
|
||||||
|
# -- Dedicated node-to-node cluster bus port.
|
||||||
|
busPort: 16379
|
||||||
|
nodeTimeoutMilliseconds: 10000
|
||||||
|
requireFullCoverage: true
|
||||||
|
allowReadsWhenDown: false
|
||||||
|
migrationBarrier: 1
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: redis
|
||||||
|
tag: 7.4-alpine
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
imagePullSecrets: []
|
||||||
|
nameOverride: ""
|
||||||
|
fullnameOverride: ""
|
||||||
|
|
||||||
|
auth:
|
||||||
|
enabled: false
|
||||||
|
# -- Password. Leave empty to auto-generate one (persisted across upgrades
|
||||||
|
# via helm lookup).
|
||||||
|
password: ""
|
||||||
|
# -- Use an existing secret instead of creating one.
|
||||||
|
existingSecret: ""
|
||||||
|
existingSecretKey: redis-password
|
||||||
|
|
||||||
|
redis:
|
||||||
|
port: 6379
|
||||||
|
# -- Cap Redis memory usage; keep it below the container memory limit.
|
||||||
|
maxmemory: 384mb
|
||||||
|
maxmemoryPolicy: noeviction
|
||||||
|
# -- Extra lines appended verbatim to redis.conf.
|
||||||
|
extraConfig: ""
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 70m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
memory: 512Mi
|
||||||
|
|
||||||
|
probes:
|
||||||
|
redis:
|
||||||
|
# -- Redis may need to recover AOF and rejoin the cluster after a worker or
|
||||||
|
# storage outage. Startup failures are tolerated for 15 minutes.
|
||||||
|
startup:
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 4
|
||||||
|
failureThreshold: 180
|
||||||
|
# -- Ten consecutive failed checks over 150 seconds are required before
|
||||||
|
# Kubernetes restarts Redis. LOADING and CLUSTERDOWN remain live states.
|
||||||
|
liveness:
|
||||||
|
periodSeconds: 15
|
||||||
|
timeoutSeconds: 8
|
||||||
|
failureThreshold: 10
|
||||||
|
# -- Readiness only controls traffic; it never restarts the container.
|
||||||
|
readiness:
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 4
|
||||||
|
failureThreshold: 3
|
||||||
|
|
||||||
|
# Redis Cluster clients must reach the exact member named by MOVED/ASK replies.
|
||||||
|
# When enabled, the Kubernetes LoadBalancer implementation allocates one
|
||||||
|
# endpoint per member plus a balanced bootstrap endpoint. Pods discover their
|
||||||
|
# assigned endpoint from the Kubernetes API before Redis starts.
|
||||||
|
externalAccess:
|
||||||
|
# -- Disabled by default because this creates one LoadBalancer per Redis
|
||||||
|
# member plus one bootstrap LoadBalancer and can incur provider charges.
|
||||||
|
enabled: false
|
||||||
|
port: 6379
|
||||||
|
bootstrap:
|
||||||
|
annotations: {}
|
||||||
|
memberServices:
|
||||||
|
annotations: {}
|
||||||
|
loadBalancerClass: ""
|
||||||
|
externalTrafficPolicy: Cluster
|
||||||
|
# -- Strongly recommended: restrict this to the client networks. An empty
|
||||||
|
# list leaves reachability to the LoadBalancer/firewall implementation.
|
||||||
|
loadBalancerSourceRanges: []
|
||||||
|
allocateLoadBalancerNodePorts: true
|
||||||
|
endpointDiscovery:
|
||||||
|
image:
|
||||||
|
repository: curlimages/curl
|
||||||
|
tag: 8.21.0
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 5m
|
||||||
|
memory: 8Mi
|
||||||
|
limits:
|
||||||
|
memory: 32Mi
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
enabled: true
|
||||||
|
image:
|
||||||
|
# -- redis_exporter's own release registry (upstream project; also mirrored
|
||||||
|
# at quay.io/oliver006/redis_exporter and docker.io/oliver006/redis_exporter).
|
||||||
|
# GHCR avoids Docker Hub pull rate limits.
|
||||||
|
repository: ghcr.io/oliver006/redis_exporter
|
||||||
|
tag: v1.87.0-alpine
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
port: 9121
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 25m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
memory: 64Mi
|
||||||
|
serviceMonitor:
|
||||||
|
# -- Requires the Prometheus Operator CRDs (kube-prometheus-stack).
|
||||||
|
enabled: false
|
||||||
|
interval: 30s
|
||||||
|
scrapeTimeout: 10s
|
||||||
|
# -- Extra labels so your Prometheus instance selects this ServiceMonitor,
|
||||||
|
# e.g. release: kube-prometheus-stack
|
||||||
|
labels: {}
|
||||||
|
|
||||||
|
# Optional browser UI for inspecting and operating this Redis Cluster. The
|
||||||
|
# component is disabled by default; enabling it creates one RedisInsight pod,
|
||||||
|
# its Service and storage, and (when requested) an Istio HTTP route.
|
||||||
|
redisInsight:
|
||||||
|
enabled: false
|
||||||
|
image:
|
||||||
|
repository: redis/redisinsight
|
||||||
|
tag: 3.8.0
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
port: 5540
|
||||||
|
connection:
|
||||||
|
# -- Empty automatically uses this release's balanced bootstrap endpoint:
|
||||||
|
# internal Service DNS normally, or the discovered LoadBalancer ingress
|
||||||
|
# address when external access is enabled. Set a hostname/IP to override.
|
||||||
|
host: ""
|
||||||
|
# -- Zero automatically follows externalAccess.port or redis.port.
|
||||||
|
port: 0
|
||||||
|
alias: redis-cluster
|
||||||
|
tls: false
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
# -- Empty uses the cluster's default StorageClass.
|
||||||
|
storageClass: ""
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
size: 1Gi
|
||||||
|
annotations: {}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
memory: 512Mi
|
||||||
|
podAnnotations: {}
|
||||||
|
nodeSelector: {}
|
||||||
|
tolerations: []
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
host: ""
|
||||||
|
gatewaySelector:
|
||||||
|
istio: ingressgateway
|
||||||
|
requestTimeout: 60s
|
||||||
|
tls:
|
||||||
|
enabled: false
|
||||||
|
# -- Name of an existing TLS Secret available to the Istio ingress
|
||||||
|
# gateway. Required when TLS is enabled.
|
||||||
|
credentialName: ""
|
||||||
|
certificate:
|
||||||
|
# -- Ask cert-manager to create credentialName. Leave false when the
|
||||||
|
# Secret is managed outside this chart.
|
||||||
|
create: false
|
||||||
|
issuerName: ""
|
||||||
|
issuerKind: ClusterIssuer
|
||||||
|
# -- Empty creates the Certificate in the release namespace. Some
|
||||||
|
# Istio installations require gateway credentials in istio-system;
|
||||||
|
# set that namespace explicitly when needed.
|
||||||
|
secretNamespace: ""
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
# -- Empty uses the cluster's default StorageClass.
|
||||||
|
storageClass: ""
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
# -- Requested capacity for every Redis member PVC. The chart imposes no
|
||||||
|
# upper bound; the StorageClass and cluster quota decide what is available.
|
||||||
|
size: 3Gi
|
||||||
|
annotations: {}
|
||||||
|
|
||||||
|
istio:
|
||||||
|
# -- Keep the Envoy sidecar OFF the Redis pods (recommended). Redis Cluster
|
||||||
|
# uses direct pod-to-pod client and cluster-bus connections. Client pods can reach
|
||||||
|
# Redis from inside the mesh (mTLS in PERMISSIVE mode, or add a
|
||||||
|
# PeerAuthentication exception for this workload if you run STRICT).
|
||||||
|
injectSidecar: false
|
||||||
|
|
||||||
|
networkPolicy:
|
||||||
|
# -- Standard Kubernetes NetworkPolicy. Enforcement depends on the installed
|
||||||
|
# CNI. Pod-to-pod client, replication, key-migration, and cluster-bus traffic
|
||||||
|
# is always allowed.
|
||||||
|
enabled: false
|
||||||
|
# -- Allow any pod in the cluster to reach the Redis client port.
|
||||||
|
# Set to false and fill extraIngress to lock down clients.
|
||||||
|
allowExternal: true
|
||||||
|
# -- Extra "from" peers allowed on the client ports when allowExternal=false,
|
||||||
|
# e.g. [{namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: myapp}}}]
|
||||||
|
extraIngress: []
|
||||||
|
# -- Namespaces allowed to scrape metrics (empty = any).
|
||||||
|
metricsFromNamespaces: []
|
||||||
|
|
||||||
|
podDisruptionBudget:
|
||||||
|
enabled: true
|
||||||
|
maxUnavailable: 1
|
||||||
|
|
||||||
|
serviceAccount:
|
||||||
|
create: true
|
||||||
|
name: ""
|
||||||
|
annotations: {}
|
||||||
|
|
||||||
|
nodeSelector: {}
|
||||||
|
tolerations: []
|
||||||
|
topologySpreadConstraints: []
|
||||||
|
priorityClassName: ""
|
||||||
|
|
||||||
|
podAnnotations: {}
|
||||||
|
podLabels: {}
|
||||||
|
|
||||||
|
podSecurityContext:
|
||||||
|
fsGroup: 999
|
||||||
|
runAsNonRoot: true
|
||||||
|
|
||||||
|
containerSecurityContext:
|
||||||
|
runAsUser: 999
|
||||||
|
runAsGroup: 999
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
|
||||||
|
terminationGracePeriodSeconds: 30
|
||||||
Reference in New Issue
Block a user