> ## Documentation Index
> Fetch the complete documentation index at: https://controlplanecorporation-majid-docs-content-expansion.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Redis Multi-Location

> Deploy one Redis or Valkey master-replica cluster stretched across Control Plane locations using the Template Catalog. Covers the engine choice, cross-region replication, Sentinel failover, authentication, public access, and backups.

<Warning>
  **Version 3.0.0 no longer creates a GVC — it deploys into one you already have, and there is no in-place upgrade path from 2.x.** A `helm upgrade` across that boundary deletes the GVC the old release created and every workload, volume set and identity inside it. See [Migrating From Version 2](#migrating-from-version-2).
</Warning>

## Overview

Redis Multi-Location deploys a single Redis Sentinel cluster spanning multiple Control Plane locations within one GVC. All replicas across all locations belong to the same cluster — replica 0 in the first configured location starts as the master, and Sentinel automatically elects a new master from any location on failure.

Since template version 2.2.0 the same cluster can run **[Valkey](https://valkey.io/)** instead of Redis. It is one value, chosen at install, and it changes nothing else about the deployment — see [Redis or Valkey](#redis-or-valkey).

<Note>
  This template deploys into a GVC **you already have**, and that GVC must have at least 2 locations. It does not create, provision or manage a GVC. For a cluster inside a single location, use [Redis](/template-catalog/templates/redis) instead — that template is a full single-location Sentinel cluster.
</Note>

### How Many Locations You Need

A failover needs a **majority of Sentinels** to agree, and Sentinel runs exactly one instance per location. That arithmetic decides what the cluster survives.

| Locations | Majority | Location losses survived | What happens when one location is lost                                                        |
| --------- | -------- | ------------------------ | --------------------------------------------------------------------------------------------- |
| **2**     | 2        | **0**                    | Surviving replicas hold the data, but **no automatic failover** — the vote cannot be reached. |
| **3**     | 2        | **1**                    | **Automatic failover.** A replica in a surviving location is promoted.                        |
| **5**     | 3        | **2**                    | Survives losing **two** locations.                                                            |

With N locations you survive `floor((N-1)/2)` losses, so an even count buys nothing over the odd count below it.

### What Gets Created

* **Stateful Redis Workload** — `{release}-redis`, running `redis.replicasPerLocation` instances in **every** configured location. One instance is the master; the rest replicate from it, across locations. Each instance gets its own persistent volume and is individually addressable.
* **Stateful Sentinel Workload** — `{release}-sentinel`, exactly 1 instance per location (not configurable). Monitors the master, holds the failover vote, and persists the elected master and its peer list to its own volume.
* **Volume Sets** — `{release}-redis-vs` for Redis data (configurable capacity and autoscaling) and `{release}-sentinel-vs` (10 GiB) for Sentinel's rewritten configuration.
* **Secrets** — Opaque secrets holding the Redis and Sentinel base configurations, mounted into each container. **Passwords are not created by this template** — see [Prerequisites](#prerequisites).
* **Identities & Policies** — A separate identity and policy per tier, each with `reveal` on exactly the secrets that tier reads, plus a bucket-scoped cloud binding when backups are enabled. The Redis tier gets one more policy granting `view` on **exactly the one GVC this release installs into**, which is what lets each instance check that GVC's location list at boot. It is scoped to that single GVC, never to the whole org.
* **Domain** *(optional)* — One domain per workload when public access is enabled, exposing Redis and Sentinel at the configured addresses.
* **Backup Cron Workload** *(optional)* — `{release}-redis-backup`, a scheduled job that writes a compressed snapshot to AWS S3 or GCS. Runs in the first configured location only.

No GVC resource is created. Every resource above lands in the GVC you install into, and nothing runs in that GVC's other locations.

## Prerequisites

**An existing GVC with at least 2 locations, and `global.locations` listing exactly the locations you want this release to run in.** Every name you list must already be one of that GVC's locations — read them back with `cpln gvc get GVC_NAME -o yaml` and compare `spec.staticPlacement.locationLinks`. Extra locations in the GVC are fine; nothing runs in them. See [Matching the Location List to the GVC](#matching-the-location-list-to-the-gvc).

A default install needs nothing else. Passwords, backups and public access each add a step:

<Steps>
  <Step title="Create the password secrets (only if you want authentication)">
    Passwords are never Helm values, so they never land in the release. Each one is an [opaque secret](/guides/create-secret/opaque) with `encoding: plain` whose **payload is the password itself** — one value, no keys:

    ```bash theme={null}
    # The Redis password (redis.passwordSecretName)
    printf '%s' "$(openssl rand -hex 24)" | cpln secret create-opaque --name my-redis-password --encoding plain -f -

    # Sentinel's own password, independent of the one above (sentinel.passwordSecretName)
    printf '%s' "$(openssl rand -hex 24)" | cpln secret create-opaque --name my-redis-sentinel-password --encoding plain -f -
    ```

    Set `redis.passwordSecretName` and `sentinel.passwordSecretName` to the names you used, and read a password back later with `cpln secret reveal my-redis-password`. Leave either value empty to run that tier without a password.
  </Step>

  <Step title="Create a bucket and cloud account (only for backups)">
    See [Backup](#backup) for the per-provider steps.
  </Step>

  <Step title="Add DNS records, and enable a dedicated load balancer on the GVC (only for public access)">
    Both must be in place **before** the deploy that turns public access on. See [Public Access](#public-access).
  </Step>
</Steps>

<Warning>
  Create the password secrets **before** installing. Without them `helm install` still reports success, but the workload waits on a secret that does not exist and `cpln logs` returns nothing at all — the reason appears only in `cpln workload get-deployments WORKLOAD --gvc GVC_NAME -o yaml`, under `status.versions[].message`. Creating the secret afterwards clears the wedge on its own within several minutes.
</Warning>

## Installation

To install, follow the instructions for your preferred method:

<CardGroup cols={2}>
  <Card title="UI" href="/template-catalog/install-manage/ui" icon="laptop">
    Browse, install, and manage templates visually
  </Card>

  <Card title="CLI" href="/template-catalog/install-manage/cli" icon="terminal">
    Manage templates from your terminal
  </Card>

  <Card title="Terraform" href="/template-catalog/install-manage/terraform" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill-rule="evenodd"><path d="M77.941 44.5v36.836L46.324 62.918V26.082zm0 0" fill="#5c4ee5"/><path d="M81.41 81.336l31.633-18.418V26.082L81.41 44.5zm0 0" fill="#4040b2"/><path d="M11.242 42.36L42.86 60.776V23.941L11.242 5.523zm0 0M77.941 85.375L46.324 66.957v36.82l31.617 18.418zm0 0" fill="#5c4ee5"/></g></svg>}>
    Declare templates in your Terraform configurations
  </Card>

  <Card
    title="Pulumi"
    href="/template-catalog/install-manage/pulumi"
    icon={<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Pulumi-Icon--Streamline-Svg-Logos" height="24" width="24">
    <desc>
        Pulumi Icon Streamline Icon: https://streamlinehq.com
    </desc>
    <path fill="#f26e7e" d="M4.683025 13.3318c0.869125 -0.5018 0.870575 -2.1264 0.003225 -3.62865s-2.27504 -2.313275 -3.1441725 -1.811475C0.672945 8.3935 0.6715 10.0181 1.53885 11.52035c0.86735 1.502275 2.27505 2.313275 3.144175 1.81145Zm0.0052 3.2167c0.86735 1.502275 0.865925 3.126875 -0.003225 3.628675 -0.86915 0.5018 -2.2768275 -0.309225 -3.144175 -1.81145 -0.8673525 -1.50225 -0.8659075 -3.126875 0.003225 -3.628675 0.8691325 -0.5018 2.276825 0.309225 3.144175 1.81145Zm5.922875 3.4243c0.86735 1.50225 0.8659 3.126775 -0.003225 3.62875 -0.869125 0.501775 -2.27685 -0.309325 -3.1442 -1.81155 -0.867325 -1.50225 -0.865875 -3.12685 0.00325 -3.628675 0.869125 -0.5018 2.276825 0.309225 3.144175 1.811475Zm-0.001925 -6.845275c0.86735 1.50225 0.8659 3.12685 -0.003225 3.628675 -0.869125 0.5018 -2.276825 -0.309225 -3.144175 -1.811475 -0.86735 -1.50225 -0.8659 -3.12685 0.003225 -3.62865 0.869125 -0.501825 2.276825 0.3092 3.144175 1.81145Z" stroke-width="0.25"></path>
    <path fill="#8a3391" d="M22.45775 11.524125c0.86725 -1.502225 0.865925 -3.12685 -0.003225 -3.62865 -0.869125 -0.501825 -2.276825 0.3092 -3.144175 1.811475 -0.86735 1.50225 -0.8659 3.126825 0.003225 3.62865 0.869125 0.501825 2.276825 -0.3092 3.144175 -1.811475Zm0.000175 3.2151c0.869075 0.5018 0.870625 2.1264 0.003225 3.62865 -0.86735 1.50225 -2.27505 2.313275 -3.144175 1.81145 -0.869125 -0.5018 -0.870575 -2.126425 -0.003225 -3.62865 0.86735 -1.50225 2.27505 -2.313275 3.144175 -1.81145ZM16.536225 18.157875c0.86915 0.501825 0.8706 2.126425 0.00325 3.628675 -0.86735 1.502125 -2.275075 2.313225 -3.1442 1.81145 -0.869125 -0.50175 -0.870575 -2.126425 -0.003225 -3.62865 0.867375 -1.502275 2.27505 -2.3133 3.144175 -1.811475Zm-0.003325 -6.843775c0.869125 0.5018 0.870575 2.126425 0.003225 3.628675s-2.27505 2.313275 -3.1442 1.811475c-0.869125 -0.501825 -0.870575 -2.126425 -0.003225 -3.628675 0.86735 -1.502275 2.27505 -2.313275 3.1442 -1.811475Z" stroke-width="0.25"></path>
    <path fill="#f7bf2a" d="M15.138225 2.06721c0 1.003615 -1.40625 1.817215 -3.14095 1.817215 -1.7347 0 -3.14095 -0.8136 -3.14095 -1.817215C8.856325 1.06359 10.262575 0.25 11.997275 0.25c1.7347 0 3.14095 0.81359 3.14095 1.81721ZM9.2166 5.482375c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172s1.40625 -1.817225 3.14095 -1.817225c1.7347 0 3.14095 0.8136 3.14095 1.817225Zm8.71005 1.8172c1.7347 0 3.14095 -0.813575 3.14095 -1.8172s-1.40625 -1.817225 -3.14095 -1.817225c-1.7347 0 -3.14095 0.8136 -3.14095 1.817225s1.40625 1.8172 3.14095 1.8172Zm-2.788425 1.605625c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172 0 -1.0036 1.40625 -1.8172 3.14095 -1.8172 1.7347 0 3.14095 0.8136 3.14095 1.8172Z" stroke-width="0.25"></path>
    </svg>}
  >
    Declare templates in your Pulumi programs
  </Card>
</CardGroup>

## Configuration

The default `values.yaml` for this template:

```yaml theme={null}
# ─── Locations ────────────────────────────────────────────────────────────────
# This chart deploys into the GVC you install into — it does NOT create one.
# Every location listed here MUST already exist in that GVC. The platform does
# not validate that: a location the GVC lacks is stored verbatim and is simply
# inert, so its Redis instances and its Sentinel never start while every status
# surface reads healthy — and the Sentinel quorum silently becomes unreachable,
# which is exactly the automatic failover this template exists to provide. The
# Redis containers read the GVC at boot and refuse to bootstrap in that state.
# Extra locations in the GVC are fine: nothing here runs in them.
#
# Lives under `global` so a parent chart sets the list once and Helm propagates
# it here — that is the only channel that reaches an aliased subchart, and it
# keeps this list and the parent's from ever being edited apart.
#
# Minimum 2 locations. Sentinel runs exactly one instance per location and a
# failover needs a majority of them, so an ODD count is what buys automatic
# failover: 3 survives losing one location, 5 survives losing two. TWO gives a
# replica that is warm but never promoted automatically. See the quorum table in
# the README. This chart cannot run in ONE location — use the `redis` template,
# which is a full single-location Sentinel cluster.
#
# `replicas` is NOT read here — set redis.replicasPerLocation instead.

global:
  locations:
    - name: aws-us-east-1
    - name: aws-eu-central-1
    - name: aws-us-west-2

# ─── Engine ───────────────────────────────────────────────────────────────────
# Which server this deployment runs. `redis` is the default and changes nothing.
# `valkey` runs BOTH the Redis and Sentinel tiers on the image below — Valkey is
# the BSD-licensed fork of Redis 7.2 and ships redis-server / redis-cli /
# redis-sentinel compatibility symlinks, so nothing else in this chart changes.
# Chosen at INSTALL time: an existing data directory cannot be moved between
# engines (Valkey cannot read RDB/AOF files written by Redis 7.4+).
engine: redis # redis | valkey
# Used for BOTH tiers when engine is valkey; redis.image / sentinel.image are
# then ignored. Use a DEBIAN-based tag: both tiers build their config with
# `echo "\n..."`, which busybox does not expand, so an `-alpine` tag fails at
# start with `Bad directive` (true of the redis `-alpine` tags too).
valkeyImage: valkey/valkey:8.1.9

# ─── Redis ────────────────────────────────────────────────────────────────────
redis:
  image: redis:7.4 # ignored when engine is valkey
  # Redis instances in EVERY location. Deliberately its own knob rather than
  # `global.locations[].replicas`: that list is shared with a parent chart,
  # where `replicas` already means the parent's own members per location.
  replicasPerLocation: 2
  resources:
    cpu: 200m
    memory: 256Mi
  serverCommand: redis-server # correct for both engines — the Valkey image ships a redis-server symlink
  extraArgs: "" # e.g. "--maxmemory 200mb --maxmemory-policy allkeys-lru"
  # OPTIONAL PREREQUISITE SECRET — empty means no Redis password at all.
  # An `opaque` secret (encoding `plain`) whose payload IS the password; it is
  # never a values entry, so it never lands in the Helm release. Create it
  # BEFORE install — see Prerequisites in the README.
  passwordSecretName: "" # e.g. my-redis-password
  publicAccess:
    enabled: false
    address: redis.my-domain.com # a domain you own; prove ownership before enabling
  volumeset:
    initialCapacity: 20 # GiB
    autoscaling:
      enabled: false
      maxCapacity: 100 # GiB
      minFreePercentage: 10
      scalingFactor: 1.2

# ─── Sentinel ─────────────────────────────────────────────────────────────────
# Exactly one Sentinel per location — the count is not configurable.
sentinel:
  image: redis:7.4 # ignored when engine is valkey
  resources:
    cpu: 200m
    memory: 256Mi
  extraArgs: "" # e.g. "--sentinel down-after-milliseconds mymaster 5000"
  # OPTIONAL PREREQUISITE SECRET — empty means Sentinel itself is unauthenticated.
  # Same shape as redis.passwordSecretName, and independent of it.
  passwordSecretName: "" # e.g. my-redis-sentinel-password
  publicAccess:
    enabled: false
    address: redis-sentinel.my-domain.com # a domain you own

# ─── Networking (applies to both the Redis and Sentinel workloads) ────────────
firewall:
  internalAllowType: same-gvc # options: same-gvc, same-org, workload-list
  # Only used when internalAllowType is workload-list. This release's OWN
  # workloads (Redis, Sentinel, the backup cron) are added automatically — the
  # list also governs Redis-to-Redis replication and Sentinel's monitoring of
  # both, so a list naming only clients would cut the cluster off from itself.
  # List your clients here; do not list this release's workloads.
  workloads: []
  #- //gvc/GVC_NAME/workload/WORKLOAD_NAME
  externalInboundAllowCIDR: "" # comma-separated; defaults to 0.0.0.0/0 when publicAccess is on
  externalOutboundAllowCIDR: "" # comma-separated

# ─── Backups ──────────────────────────────────────────────────────────────────
# A nightly RDB snapshot to object storage. The cron workload runs in the FIRST
# location listed above only — a cron workload otherwise fires in every location
# of its GVC and writes N copies of the same dump into one bucket.
backup:
  enabled: false
  image: ghcr.io/controlplane-com/backup-images/redis-backup:1.0.0
  schedule: "0 2 * * *" # cron schedule, default is daily at 02:00 UTC

  resources:
    cpu: 100m
    memory: 128Mi

  provider: aws # options: aws or gcp

  aws:
    bucket: my-redis-bucket
    region: us-east-1
    cloudAccountName: my-s3-cloud-account
    policyName: my-redis-backup-policy # bucket-scoped IAM policy, see README
    prefix: redis/backups # folder within the bucket

  gcp:
    bucket: my-redis-bucket
    cloudAccountName: my-gcs-cloud-account
    prefix: redis/backups # folder within the bucket
```

### Locations

The location list lives under `global` so that a parent chart can set it once when this template is used as a subchart — that is the only channel that reaches an aliased subchart, and it keeps the two lists from ever being edited apart.

* `global.locations[].name` — A Control Plane location (e.g. `aws-us-east-1`) that **must already be one of the locations of the GVC you install into**. At least 2 are required; see [How Many Locations You Need](#how-many-locations-you-need).
* The **first** location listed is where the initial master is seeded, and where the backup cron runs.

<Note>
  `global.locations[].replicas` is deliberately **not** read by this template — that field belongs to parent charts that share the same location list, where it already means the parent's own members per location. Set `redis.replicasPerLocation` instead, which applies to every location. A standalone install that sets `replicas` fails at render with a message saying so.
</Note>

### Matching the Location List to the GVC

The platform validates the pairing in neither direction. A location the GVC lacks is accepted, stored and simply inert, and both its Redis instances and its Sentinel never start — which quietly removes part of the Sentinel quorum that automatic failover depends on, while every status surface reads healthy. Extra locations in the GVC are harmless: they get `minScale`/`maxScale` of `0` and their deployments read `This workload location is deactivated because maxScale is set to 0`.

The Redis instances therefore read their own GVC at boot, using the scoped `view` grant described in [What Gets Created](#what-gets-created), and refuse to bootstrap a cluster that could never work. On a **fresh** data directory each of these is fatal — the container exits with code `1` and logs a `[redis] FATAL:` line naming the problem:

| Check                | Condition                                                   | Why it matters                                                                                                                                                                                                            |
| -------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Bootstrap location   | The **first** entry of `global.locations` is not in the GVC | Every Sentinel is seeded to monitor replica 0 in that location. The address never resolves, every instance boots as a replica, and **no master ever forms** — the cluster runs, reports healthy, and refuses every write. |
| Sentinel quorum      | Fewer locations are present than the failover vote needs    | The cluster would run with **no automatic failover at all**, which is the thing the template exists to provide.                                                                                                           |
| Any missing location | Any other declared location the GVC lacks                   | Sentinel would meet its quorum with nothing to spare, so losing any one location would end automatic failover permanently.                                                                                                |

On an **already-initialized** cluster all three are warnings only and the instances keep serving, so that losing a location cannot also take the cluster down. Read the `[redis]` lines with `cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-redis"}'`.

<Warning>
  **Before version 3.0.0 this check never actually ran.** Its request to the platform API was rejected before it could read anything, so every instance took the fail-open branch and skipped the whole thing silently. Following the earlier guidance could therefore leave you with a cluster that had no master and refused every write while both workloads reported healthy. The check works from 3.0.0 onward and has been verified on all three arms.
</Warning>

### Redis or Valkey

`engine` picks the server both tiers run. It is the only difference between the two shapes — topology, replica counts, configuration, Sentinel behavior, secrets, firewall and backups are identical, and setting `engine: valkey` changes exactly the two image references in the whole deployment.

|                | `engine: redis` (default)                      | `engine: valkey`                                                        |
| -------------- | ---------------------------------------------- | ----------------------------------------------------------------------- |
| Image          | `redis.image` / `sentinel.image` (`redis:7.4`) | `valkeyImage` (`valkey/valkey:8.1.9`) for **both** tiers                |
| License        | Redis Source Available / SSPL                  | BSD-3-Clause, Linux Foundation — no paid edition, nothing feature-gated |
| On-disk format | RDB 12                                         | RDB 11 (the Redis 7.2 format)                                           |

[Valkey](https://valkey.io/) is the fork of Redis 7.2 that the Linux Foundation stewards. Its image ships `redis-server`, `redis-cli` and `redis-sentinel` compatibility symlinks, so every command, configuration directive and connection string on this page is unchanged — including `masterauth`, `sentinel auth-pass` and the hostname-based replica discovery this template relies on across locations. `redis.serverCommand` stays `redis-server` on both engines.

* **`engine` binds both tiers at once.** You cannot end up with a Valkey server behind a Redis Sentinel by accident: setting `engine: valkey` makes `redis.image` and `sentinel.image` inert.
* **Set `valkeyImage` to a Debian-based tag.** Both tiers assemble their configuration with `echo "\n..."`, which busybox does not expand, so an `-alpine` tag fails at start with `Bad directive or wrong number of arguments`. The `redis` `-alpine` tags fail the same way.
* **Valkey 9.x tags are reachable through `valkeyImage` but have not been tested with this template.** Valkey 9 writes RDB format 80, which no Redis release can read, so it is a one-way move rather than a supported upgrade.
* **`INFO` reports `redis_version:7.2.4` on Valkey** for client compatibility. Read `server_name` and `valkey_version` to see what is really running — anything that version-gates on `redis_version` believes it is talking to Redis 7.2.
* **The marketplace card and the `app.cpln.io/version` tag still show the Redis version** (`7.4`) on a Valkey install. A chart's `appVersion` is a constant and cannot follow a values setting.

<Warning>
  **`engine` is an install-time choice and cannot be changed on an existing install.** This template ships `appendonly yes`, and `redis:7.4` writes its append-only base file in RDB format 12, which Valkey 8 refuses. `helm upgrade` **reports success**, then every Redis replica crash-loops with `Can't handle RDB format version 12` and `Error reading the RDB base file appendonly.aof.N.base.rdb, AOF loading aborted`, exiting with code 1. Setting `engine` back to `redis` and upgrading again recovers the data untouched — measured at 130 seconds with all 100 test keys intact. Move between engines with a dump and restore, or by replicating into a fresh install; never by changing the value.
</Warning>

### Authentication

Authentication is disabled by default. Redis and Sentinel take independent passwords, each supplied as the [prerequisite secret](#prerequisites) named by its own value — never as a Helm value.

* `redis.passwordSecretName` — Names the opaque secret whose payload is applied as both `requirepass` and `masterauth` on every Redis instance. Empty means no Redis password at all.
* `sentinel.passwordSecretName` — Names the opaque secret whose payload is applied as `requirepass` on the Sentinel process. Independent of the Redis password; empty means Sentinel itself is unauthenticated.

Both tiers read their secret at container start, and Sentinel rewrites its own configuration from it on every start.

<Warning>
  **Rotating a password requires a forced redeployment — it does not apply on its own.** Updating the secret in place restarts nothing: measured five minutes after a rotation, the workload version was unchanged and the **old password was still accepted**, with a healthy status throughout. There is no error anywhere, so a rotation looks like it worked while the old credential keeps working indefinitely. Apply it explicitly, Sentinel first and Redis second:

  ```bash theme={null}
  cpln workload force-redeployment RELEASE_NAME-sentinel --gvc GVC_NAME
  cpln workload force-redeployment RELEASE_NAME-redis --gvc GVC_NAME
  ```
</Warning>

<Note>
  Adding, changing or removing `redis.passwordSecretName` through a `helm upgrade` restarts every Redis instance in every location at once, which takes the master out of quorum and starts a failover. Treat it as a planned restart of the whole tier. Changing only the Sentinel password is safe — a Sentinel-only restart triggers no failover vote.
</Note>

### Redis

* `redis.image` — Redis image to use. Ignored when `engine` is `valkey`.
* `redis.replicasPerLocation` — Number of Redis instances to run in **each** location (default `2`). The total instance count is this value multiplied by the number of locations.
* `redis.resources.cpu` / `redis.resources.memory` — CPU and memory allocated per Redis instance.
* `redis.serverCommand` — The server binary to execute. `redis-server` is correct for both engines.
* `redis.extraArgs` — Additional server arguments appended to the startup command, for example `--maxmemory 200mb --maxmemory-policy allkeys-lru`. They override the equivalent settings in the base configuration.

### Sentinel

Sentinel monitors the Redis master across all locations and promotes a replica on failure. One Sentinel instance is deployed per location — this is fixed and not configurable. Quorum is calculated automatically from the number of locations. The master name is `mymaster`.

* `sentinel.image` — Image to use for Sentinel. Ignored when `engine` is `valkey`.
* `sentinel.resources.cpu` / `sentinel.resources.memory` — CPU and memory allocated per Sentinel instance.
* `sentinel.extraArgs` — Additional Sentinel arguments, for example `--sentinel down-after-milliseconds mymaster 5000`.

### Storage

* `redis.volumeset.initialCapacity` — Initial volume size in GiB for Redis data (default 20). Every Redis instance gets its own volume.
* `redis.volumeset.autoscaling.enabled` — Automatically expand the volume as it fills. When enabled:
  * `maxCapacity` — Maximum volume size in GiB.
  * `minFreePercentage` — Trigger a scale-up when free space drops below this percentage.
  * `scalingFactor` — Multiply current capacity by this factor when scaling up.

Sentinel's own 10 GiB volume, which holds its rewritten configuration, is not configurable.

### Public Access

Redis and Sentinel can be exposed over the internet as raw TCP via Control Plane's domain resource, with per-replica port routing.

<Warning>
  **A dedicated load balancer must already be enabled on your GVC, and this template can no longer enable it for you.** TCP port routing needs one, and it is a paid Control Plane feature. Earlier versions turned it on as part of creating the GVC; 3.0.0 creates no GVC, so enable it on the GVC's load balancer settings **before** the deploy that turns public access on. A domain pointed at a GVC without one will not serve traffic.
</Warning>

* `redis.publicAccess.enabled` — Expose Redis publicly (default: `false`).
* `redis.publicAccess.address` — A domain you control to expose Redis (e.g. `redis.your-domain.com`).
* `sentinel.publicAccess.enabled` — Expose Sentinel publicly (default: `false`).
* `sentinel.publicAccess.address` — A domain you control to expose Sentinel (e.g. `redis-sentinel.your-domain.com`).

DNS records must be added before deploying. If ownership has not been proven, the first deploy will fail and the error message will list the exact TXT and CNAME records required. Add those records in your DNS provider, then upgrade the release to complete the deployment. **Disable DNS proxying** (e.g. Cloudflare's orange cloud) — TCP traffic must pass through directly.

When enabled, a Control Plane domain resource is created for each address. Port mapping is one port per replica:

| Workload | Ports                                                      |
| -------- | ---------------------------------------------------------- |
| Redis    | `6380`, `6381`, ... (one per replica across all locations) |
| Sentinel | `26380`, `26381`, ... (one per location)                   |

`firewall.externalInboundAllowCIDR` defaults to `0.0.0.0/0` when public access is enabled; set it to restrict which addresses can reach the workloads.

**Connecting externally:**

```bash theme={null}
# Redis replica 0
redis-cli -h redis.your-domain.com -p 6380 ping

# Redis replica 1
redis-cli -h redis.your-domain.com -p 6381 ping

# Sentinel in the first location
redis-cli -h redis-sentinel.your-domain.com -p 26380 ping
```

Add `--no-auth-warning -a "$PASSWORD"` to either command if you set the matching password.

### Firewall

A single firewall configuration applies to both the Redis and Sentinel workloads.

* `firewall.internalAllowType` — Controls which workloads can connect internally:

| Value           | Description                                                     |
| --------------- | --------------------------------------------------------------- |
| `same-gvc`      | Allow access from all workloads in the same GVC (recommended)   |
| `same-org`      | Allow access from all workloads in the same organization        |
| `workload-list` | Allow access only from specific workloads listed in `workloads` |

* `firewall.workloads` — List of specific workload links, used when `internalAllowType` is `workload-list`.
* `firewall.externalInboundAllowCIDR` — Comma-separated list of CIDRs allowed to reach the workloads externally. Defaults to `0.0.0.0/0` when public access is enabled.
* `firewall.externalOutboundAllowCIDR` — Comma-separated list of CIDRs the workloads are allowed to connect to externally.

<Warning>
  **With `workload-list`, list only your clients — never this release's own workloads.** The same list governs Redis-to-Redis replication and Sentinel's monitoring of both tiers, so a list naming only clients used to cut the cluster off from itself while every replica still reported ready. The Redis, Sentinel and backup-cron workloads are appended for you from version 3.0.0, and a client that is not listed is still refused.
</Warning>

<Warning>
  **Do not pin `externalInboundAllowCIDR` to a single `/32` unless that address is genuinely static.** An allow list is only as stable as the egress address in it: when your office, VPN or NAT gateway rotates its IP, the entry stops matching and you lose access to your own cluster from outside. Allow the range your egress can actually come from, and keep an in-GVC path you can still reach.
</Warning>

<Note>
  A firewall change is not instant — 132 seconds was measured on this template, and longer elsewhere. Allow several minutes and re-test before concluding a setting did not apply.
</Note>

### Connecting to Redis

All replicas across all locations belong to a single cluster and are reachable from anywhere in the GVC. Replica 0 in the first configured location starts as the master; after any failover, Sentinel is the authority on where the master is.

**Option 1 — Load-balanced endpoint (any replica; only the master accepts writes):**

```text theme={null}
RELEASE_NAME-redis.GVC_NAME.cpln.local:6379
```

**Option 2 — Direct to a specific replica:**

```text theme={null}
replica-0.RELEASE_NAME-redis.LOCATION.GVC_NAME.cpln.local:6379
replica-1.RELEASE_NAME-redis.LOCATION.GVC_NAME.cpln.local:6379
```

**Option 3 — Via Sentinel, to always write to the current master:**

```bash theme={null}
MASTER_INFO=$(redis-cli -h RELEASE_NAME-sentinel.GVC_NAME.cpln.local -p 26379 SENTINEL get-master-addr-by-name mymaster)
MASTER_HOST=$(echo $MASTER_INFO | cut -d' ' -f1)
MASTER_PORT=$(echo $MASTER_INFO | cut -d' ' -f2)
redis-cli -h $MASTER_HOST -p $MASTER_PORT SET my-key "Hello world"
```

Add `--no-auth-warning -a "$SENTINEL_PASSWORD"` to the Sentinel call and `--no-auth-warning -a "$REDIS_PASSWORD"` to the Redis call when passwords are configured.

For Sentinel-aware clients, connect through:

```text theme={null}
RELEASE_NAME-sentinel.GVC_NAME.cpln.local:26379
```

A specific Sentinel is reachable at `replica-0.RELEASE_NAME-sentinel.LOCATION.GVC_NAME.cpln.local:26379`. The Sentinel master name is `mymaster`.

### Ports

| Workload          | Port                  | Protocol | Description                               |
| ----------------- | --------------------- | -------- | ----------------------------------------- |
| Redis             | `6379`                | TCP      | Redis data port (internal)                |
| Sentinel          | `26379`               | TCP      | Sentinel discovery port (internal)        |
| Redis (public)    | `6380`, `6381`, ...   | TCP      | One port per replica across all locations |
| Sentinel (public) | `26380`, `26381`, ... | TCP      | One port per location                     |

## Migrating From Version 2

Every release before 3.0.0 created its own GVC. **Do not `helm upgrade` a 2.x release onto 3.0.0.** Once the chart stops declaring a GVC, Helm prunes the one the old release created — and deleting a GVC deletes every workload, volume set and identity inside it, in seconds, while the command prints `upgraded successfully`.

<Warning>
  The chart refuses to render when your values still carry the 2.x `global.gvc` key, so a values-carrying upgrade fails safely before any resource is touched. That guard **cannot** fire on an upgrade run with no values file at all, because it then sees only 3.0.0's own defaults. The procedure below is the safety; the render guard is only a backstop.
</Warning>

Install 3.0.0 as a **new release against an existing GVC**, copy the keyspace across, then remove the old release.

<Steps>
  <Step title="Pick the target GVC">
    It needs at least two locations, and every entry you put in `global.locations` must be one of them.
  </Step>

  <Step title="Install 3.0.0 as a new release, keeping the old one running">
    Delete `global.gvc.name` from your values and rename `global.gvc.locations` to a top-level `global.locations`.
  </Step>

  <Step title="Move the data">
    Point a client at the old cluster's current master and the new one's, and copy the keyspace across — `redis-cli --scan` plus `DUMP`/`RESTORE`, or replication from the old master, whichever suits your dataset. Confirm the new cluster serves reads and writes before going on.
  </Step>

  <Step title="Cut clients over">
    Every internal hostname contains the GVC name, so every connection string changes.
  </Step>

  <Step title="Remove the old release">
    Uninstalling it deletes the GVC it created and the old volume sets with it, so make sure the previous step is genuinely complete and nothing else depends on that GVC.
  </Step>
</Steps>

Values that changed:

| 2.x                    | 3.0.0                                     |
| ---------------------- | ----------------------------------------- |
| `global.gvc.name`      | Removed — the GVC is wherever you install |
| `global.gvc.locations` | `global.locations`                        |

## Availability and Failover

Measured on a two-location cluster (`aws-us-east-1` and `aws-us-west-2`) running Valkey 8.1.9 with `replicasPerLocation: 2` — one master and three replicas — and a one-second write probe connecting through Sentinel.

| Event                                                      | Measured impact                                                                                                                                              |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Cold install until every replica is ready                  | **96 s** on Valkey, **63 s** on the default Redis engine                                                                                                     |
| Cross-region replication of a write                        | `WAIT 3 1000` returned `3` — both replicas in the **other** region acknowledged the write within one second                                                  |
| Loss of the instance holding the master role               | Sentinel went from `+sdown` to `+failover-end` in **2.3 s**; clients saw an **8.0 s** write gap; the stopped instance rejoined as a replica with no key loss |
| `helm upgrade`, or a forced redeployment of the Redis tier | Every replica restarts at once: an **8.45 s** write gap, and the master moved to the other region                                                            |

<Warning>
  **Treat every `helm upgrade` as a planned write outage and a change of master.** Instances do not restart one at a time — the field that would serialize the rollout is not retained by the platform on a stateful workload, so all of them go down together and Sentinel elects a new master when they return.
</Warning>

A three-location run of 3.0.0 (`aws-us-east-1`, `aws-us-east-2`, `aws-us-west-2`, `replicasPerLocation: 2`) measured the case the template exists for: with the master taken out, **a replica in a different location was promoted in 8–9 seconds** across two runs, all three Sentinels converged on the same new master with no split brain, pre-failover data survived the promotion, and the old master rejoined as a replica of the new one and received the writes it had missed.

<Note>
  The two-location measurements above are the loss of the **master instance**, not of a whole location — a two-location cluster cannot fail over on location loss, because its Sentinel quorum is 2 of 2. Use three or more locations to survive losing one.
</Note>

Replication is **asynchronous**, so a failover can lose writes the promoted replica had not yet received, bounded by the replication lag at the moment of failure.

## Backup

Backup is disabled by default. When enabled, a cron workload runs in the first configured location only and uploads a single compressed `redis-<timestamp>.rdb.gz` snapshot to AWS S3 or GCS. It runs in one location because a cron workload otherwise fires in every location of its GVC and writes one copy of the same dump per location into the same bucket.

* `backup.enabled` — Enable scheduled backups.
* `backup.image` — The backup container image.
* `backup.schedule` — Cron expression for backup frequency (default: daily at 02:00 UTC).
* `backup.provider` — `aws` or `gcp`.
* `backup.resources.cpu` / `backup.resources.memory` — Resources for the backup cron container.

<Note>
  The job connects to the load-balanced Redis endpoint, so the snapshot is taken from whichever instance answers — usually a replica rather than the master. Replicas apply writes asynchronously, so a snapshot can be marginally behind the master at the instant it was taken.
</Note>

### AWS S3

Before enabling backup with `provider: aws`, complete the following in your AWS account:

1. Create an S3 bucket. Set `backup.aws.bucket` to its name and `backup.aws.region` to its region.
2. If you do not have a Cloud Account set up, refer to the docs to [Create a Cloud Account](/guides/create-cloud-account). Set `backup.aws.cloudAccountName` to its name.
3. Create an IAM policy with the following JSON, replacing `YOUR_BUCKET_NAME`:

```json theme={null}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:ListBucket",
                "s3:GetObjectVersion",
                "s3:DeleteObjectVersion"
            ],
            "Resource": [
                "arn:aws:s3:::YOUR_BUCKET_NAME",
                "arn:aws:s3:::YOUR_BUCKET_NAME/*"
            ]
        }
    ]
}
```

4. Set `backup.aws.policyName` to the name of the policy created in step 3. This bucket-scoped policy is all the workload identity needs — no broad managed policy is required.
5. Set `backup.aws.prefix` to the folder path where backups will be stored.

### GCS

Before enabling backup with `provider: gcp`, complete the following in your GCP account:

1. Create a GCS bucket. Set `backup.gcp.bucket` to its name.
2. If you do not have a Cloud Account set up, refer to the docs to [Create a Cloud Account](/guides/create-cloud-account). Set `backup.gcp.cloudAccountName` to its name.
3. Add the **Storage Admin** role to the GCP service account associated with the Cloud Account. The template additionally binds the identity to `roles/storage.objectAdmin` on exactly the bucket named in `backup.gcp.bucket`.
4. Set `backup.gcp.prefix` to the folder path where backups will be stored.

## Restoring a Backup

The backup produces a single file (`redis-<timestamp>.rdb.gz`).

<Warning>
  **Restoring is not a one-liner here, and the obvious approach silently does nothing.** This template runs with `appendonly yes`, so Redis loads `appendonlydir/` at start and **ignores `dump.rdb` entirely** — copying a downloaded RDB into the data directory changes nothing, and reports no error. The Redis image also ships neither `aws` nor `gsutil`, so the download happens outside the container.
</Warning>

Fetch the object first:

```sh theme={null}
aws s3 cp s3://BUCKET_NAME/PREFIX/BACKUP_FILE.rdb.gz - | gunzip > ./dump.rdb   # AWS S3
gsutil cp gs://BUCKET_NAME/PREFIX/BACKUP_FILE.rdb.gz - | gunzip > ./dump.rdb   # GCS
```

Loading it then requires starting the instance with AOF disabled so the RDB is read, and re-enabling AOF afterwards so it is rewritten from memory. **That sequence has not been verified against this template.** Rehearse it against a scratch install before you need it, rather than first attempting a restore during an incident.

## Important Notes

* **There is no upgrade path from 2.x.** Every 2.x release created its own GVC; 3.0.0 deploys into an existing one, and a `helm upgrade` across that boundary deletes the old GVC and everything in it. The chart refuses to render on the 2.x `global.gvc` key, but that guard cannot see an upgrade run with no values at all. Follow [Migrating From Version 2](#migrating-from-version-2).
* **Every location in `global.locations` must already exist in the GVC you install into.** The platform accepts one that does not, stores it and runs nothing there — which silently removes part of the Sentinel quorum. A fresh install now refuses to bootstrap and names the problem; an already-initialized cluster warns and keeps serving. See [Matching the Location List to the GVC](#matching-the-location-list-to-the-gvc).
* **Choose `engine` before the first install.** It cannot be changed afterwards: a Valkey server refuses the on-disk format Redis 7.4 wrote, `helm upgrade` reports success anyway, and every replica crash-loops until the value is set back. See [Redis or Valkey](#redis-or-valkey).
* **Create the password secrets before installing.** `redis.passwordSecretName` and `sentinel.passwordSecretName` name secrets this template does not create; pointing either at a secret that does not exist wedges the deployment waiting on it.
* **Rotating a password needs a forced redeployment.** Updating the secret in place changes nothing, and the old password keeps working with every status surface reporting healthy.
* **Every `helm upgrade` restarts every replica in every location at once**, costing a short write outage and a change of master.
* **Two locations give no automatic failover**, because the Sentinel vote cannot reach a majority. Use three or more to survive losing a location.
* **With `firewall.internalAllowType: workload-list`, list only your clients** — this release's own workloads are added for you. Listing only clients used to cut the cluster off from itself while every replica still reported ready.
* **Public access needs a dedicated load balancer you enable on the GVC yourself.** This template no longer creates the GVC and so can no longer turn it on; without one, the domain is created and never serves traffic.
* **Never suspend a location.** Suspending and resuming one permanently withdraws its endpoints from the other locations' service discovery while every status surface still reports healthy. To remove a location, take it out of `global.locations`.
* **Extra locations in the GVC are harmless.** A location this release does not declare starts nothing; its deployment reads `This workload location is deactivated because maxScale is set to 0`.
* **Allow about two minutes after a cold install** before believing a replica is unreachable — cross-region service discovery takes that long to converge, and a firewall change can take a further few minutes.
* **Data survives an upgrade but not an uninstall** — `helm uninstall` deletes both volume sets.

## External References

<CardGroup cols={2}>
  <Card title="Redis Documentation" icon="book" href="https://redis.io/docs/latest/">
    Official Redis documentation
  </Card>

  <Card title="Redis Sentinel Documentation" icon="book" href="https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/">
    Redis Sentinel setup and client configuration
  </Card>

  <Card title="Valkey Documentation" icon="book" href="https://valkey.io/topics/">
    Official Valkey documentation and configuration reference
  </Card>

  <Card title="Redis Persistence" icon="floppy-disk" href="https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/">
    How RDB and AOF files are written and loaded
  </Card>

  <Card title="Backup Image Source" icon="github" href="https://github.com/controlplane-com/backup-images/tree/main/redis-backup">
    Source code for the Redis backup container image
  </Card>

  <Card title="Redis Multi-Location Template" icon="github" href="https://github.com/controlplane-com/templates/tree/main/redis-multi-location">
    View the source files, default values, and chart definition
  </Card>
</CardGroup>
