Overview
etcd is a strongly consistent key-value store used as the coordination layer for leader election, distributed locking, and shared configuration. This template deploys a single stretched etcd cluster with exactly one member per Control Plane location, sharing one Raft quorum across regions, with Raft timers tuned for cross-region round trips and auto-compaction enabled so the backend does not grow without bound. It is also the consensus store that Postgres Multi-Location bundles for Patroni, and is independently useful as a cross-region coordination store for your own services.global.locations must already be one of them. A GVC location you do not list runs nothing — its deployment reads This workload location is deactivated because maxScale is set to 0. For a cluster in a single location use the etcd template instead.What Gets Created
RELEASE_NAME-etcd-LOCATION, so etcdctl member list maps one-to-one onto your location list.
Prerequisites
A GVC with at least two locations, covering every entry in global.locations
spec.staticPlacement.locationLinks is the list. Every name you put in global.locations must appear there — a declared location the GVC lacks makes a fresh member refuse to start (see Troubleshooting). Three locations is the recommended production shape; see Locations and Quorum.Installation
The defaultglobal.locations list (aws-us-east-1, aws-us-west-2) is only the smallest cluster the chart allows — set it to your GVC’s own locations in a values file, then install:
UI
CLI
Terraform
Pulumi
Configuration
Each subsection below shows the shipped default for one area ofvalues.yaml. Override only what you need with a values file or --set.
Locations and Quorum
global.locations— The member map: one etcd member is deployed in each listed location, and at least two locations are required. Every name must already be a location of the GVC you install into. Each location runs exactly one member; areplicasvalue other than1fails the render in a standalone install.
global so a parent chart — Postgres Multi-Location bundles this template as a subchart — sets it once and Helm propagates it here; the two lists can then never be edited apart.
etcd commits a write only when a majority of members accept it. Because there is exactly one member per location, the location is the failure domain:
N locations you survive floor((N-1)/2) losses, so an even count never buys anything over the odd count below it. Two locations is permitted as a deliberate warm-standby topology, but it survives nothing automatically.
global.locations on a running release reprovisions the cluster: every member restarts with a new cluster list. This is not etcd’s graceful member add / member remove path, so plan it as a maintenance window — and change the GVC’s locations and this list together.Image and Resources
image— The etcd container image. The shipped tag carries etcd3.6.5together withetcdctlandetcdutl.resources.cpu/resources.memory— CPU and memory limit for each member. See the etcd hardware recommendations for sizing guidance.
Raft Tuning
tuning.heartbeatIntervalMs— Leader heartbeat interval, roughly 0.5–1.5× the worst round trip between your locations.tuning.electionTimeoutMs— How long a follower waits without a heartbeat before campaigning. Must be at least 10× the heartbeat interval and at most50000; both bounds are enforced at render time.
Compaction and Backend Growth
etcd never discards a superseded revision on its own. Every write creates a new revision and the old one is kept until a compaction removes it, so the backend grows with time alone whenever a client writes on a timer — Patroni, for example, renews its leader lease every few seconds, producing new revisions whether or not any application data changes. Left uncompacted, an otherwise idle cluster reaches etcd’s default 2 GiB backend quota in a matter of months. When the quota is reached, etcd raises a cluster-wideNOSPACE alarm and every member goes read-only until an operator intervenes. Auto-compaction is what prevents that, and this template configures it by default:
tuning.autoCompactionMode— HowautoCompactionRetentionis read.periodictreats it as a time window;revisiontreats it as a number of revisions to keep. There is no “off” — an unrecognized mode is rejected at render time.tuning.autoCompactionRetention— How much history is kept before old revisions are discarded. The default1hkeeps the backend flat. Raising it to24hor more buys a longer history window at the cost of a larger backend.tuning.quotaBackendBytes— Backend size ceiling in bytes.0(the default) means the flag is not passed at all and etcd applies its own 2 GiB limit. Leave it at0unless the keyspace genuinely outgrows that; 8 GiB (8589934592) is etcd’s own suggested maximum, above which it warns at startup.
0, an unrecognized mode and a negative quota (which etcd reads as “no quota at all”) are each rejected at render time, because each one produces a chart that installs cleanly and fails weeks later.
DB SIZE therefore plateaus rather than dropping, while DB SIZE IN USE falls back to the size of the live keyspace. That is expected and is sufficient to stay under the quota, because reclaimed pages are reused for new writes. Only etcdctl defrag returns space to the filesystem, and this template does not automate it — defragmentation blocks the member it runs on.etcdctl endpoint status reports QUOTA | 0 B when quotaBackendBytes is 0, because that column reflects the flag rather than the effective limit. The effective quota in that case is still etcd’s 2 GiB default, which etcd logs at startup as enabled backend quota with default value.Storage
volumeset.capacity— Size in GiB of the persistent volume each member gets for/var/lib/etcd(minimum 10). The volume set keeps snapshots for 7 days and takes a final snapshot when the release is uninstalled.
Internal Access
internalAccess.type— Controls which workloads can reach the client API on port2379. Traffic between locations inside one GVC is same-GVC traffic, so the default covers a stretched cluster with no extra rule.
internalAccess.workloads— The workload links allowed in whentypeisworkload-list. List only your clients. The members of this workload must also be able to reach each other, so the template appends its own workload link to the list for you; a client that is not listed is still refused, and the cluster stays whole.
Disaster Recovery
recovery.forceNewClusterInLocation— Leave empty in normal operation. Set it to the name of the one surviving location only after quorum is permanently lost, following Recovering From a Lost Location, and clear it again afterwards. The value must be one of the names inglobal.locations; anything else is rejected at render time.
Connecting
The cluster has no public endpoint: external inbound traffic is closed and no load balancer is created. Clients connect from inside the GVC, or from your machine through a port-forward.etcdctl is in the image and talks to the local member by default:
member list should show one member per configured location, each named RELEASE_NAME-etcd-LOCATION, and endpoint status --cluster should show every member on the same RAFT TERM with exactly one IS LEADER: true. Then write and read a key:
etcdctl (or your client library) at the service address:
etcdctl against http://127.0.0.1:2379:
Operations
Backing Up
Two independent layers protect the data:-
Volume set snapshots. The volume set
RELEASE_NAME-etcd-vskeeps snapshots for 7 days and takes a final snapshot when the release is uninstalled; this is configured by the template and needs no action. To take one on demand in a given location: -
An etcd snapshot file.
etcdctl snapshot savewrites a consistent, hash-protected copy of the whole keyspace from one member — any member will do, since every member holds the full keyspace. The image shipsetcdctlandbase64, so the file can be pulled out throughcpln workload exec. Pass it throughbase64on both sides — the exec stream is text-only and corrupts raw binary:Then confirm the file is intact on your machine withetcdutl, which is part of every etcd release:
etcdctl documentation, but has not been exercised against a live install of this template. Run it once on a test release and confirm etcdutl snapshot status reports the hash before you rely on it.Restoring a Backup
Restoring is not a procedure this template performs or has verified. The upstream path —etcdutl snapshot restore — rebuilds a data directory for every member with a new cluster ID and requires each member to be stopped and started from the rebuilt directory. This template manages the data directory through its startup script and gives you no supported way to stop a member and replace /var/lib/etcd on its volume. Restoring a volume set snapshot in place is likewise unverified for this template.
If you must recover a keyspace, follow the etcd disaster recovery guide against a new release and rehearse it on a throwaway release first. For the loss of a minority or a majority of locations with the surviving members intact, see Recovering From a Lost Location instead — that path needs no snapshot. Where etcd came bundled with Postgres Multi-Location as its Patroni consensus store, consult that page before attempting any etcd-level recovery.
Migrating From Version 1
Versions before 2.0.0 created their own GVC; 2.0.0 deploys into one you already have. Install 2.0.0 as a new release against an existing GVC, move the keyspace across, then remove the old release.Choose the target GVC
cpln gvc get GVC_NAME -o yaml — spec.staticPlacement.locationLinks is the list. Do not point the new release at the GVC the old release created: that GVC is still owned by the old Helm release, and uninstalling it later would take the new workloads with it.Export the old cluster's keyspace
etcdctl snapshot save file as shown in Backing Up, if you prefer a hash-protected copy to keep.Install 2.0.0 as a new release
global.gvc.name and global.gvc.locations in your old values with a single top-level global.locations list naming the chosen GVC’s locations, then install into that GVC as shown in Installation.Confirm the new cluster, then write your keys into it
etcdctl member list -w table must show one member per configured location before you load anything. Write the exported keys with etcdctl put from your client, then point your clients at the new release’s address.Remove the old release
Scaling and Availability
- Fault tolerance follows the location count, not the template. The cluster keeps serving while a majority of members is up: 3 locations tolerate the loss of 1, 5 tolerate 2, and 2 tolerate none (see Locations and Quorum). If you want automatic failover, use three locations.
- Losing a minority of locations is survived automatically. The remaining members elect a new leader among themselves and keep committing writes; a member that comes back with its volume intact rejoins on its own.
- Losing a majority stops writes but loses no committed data. The survivors hold the current keyspace and wait for quorum. Writes hang rather than failing fast, so clients need a command timeout; serializable reads (
--consistency=s) keep succeeding against stale data. If the lost locations are not coming back, follow Recovering From a Lost Location. - A
cpln helm upgraderestarts the members together — including an upgrade that changes nothing. Each member rejoins from its own volume and the keyspace is preserved, but expect a brief write outage while a leader is re-elected, so plan every upgrade as a short planned outage. - Change the location count only as a planned reprovision. Editing
global.locationsrestarts every member with a new cluster list; the template performs noetcdctl member addormember remove. Change the GVC’s locations and the list together. - Do not run more than one member per location.
global.locations[].replicasmust be1; a second member in a location adds cost and makes that location’s loss count double against quorum.
Recovering From a Lost Location
recovery.forceNewClusterInLocation is for the case where quorum is permanently gone — with two locations, that is the loss of either one. It is not needed for a location that is coming back: a member that returns with its volume intact rejoins on its own.
Confirm the loss is permanent
etcdctl member remove cannot help here, because removing a member itself requires quorum. If the location may return, wait for it instead.Force a new single-member cluster
recovery.forceNewClusterInLocation to the surviving location’s name and upgrade the release:Clear the setting
recovery.forceNewClusterInLocation back to "" and upgrade again. Leaving it set means the flag fires on every future restart of that member.Reset the lost location's volume before it rejoins
global.locations, not against the GVC. Naming a location the GVC does not have therefore passes validation, no member runs there, and the flag is never applied. Every surviving member logs a warning saying exactly that — see Troubleshooting if a recovery appears to do nothing.If the Backend Quota Is Already Full
Every version of this template compacts, so a default install does not accumulate revisions in the first place. A backend can still reach the quota if the keyspace itself is genuinely large, or ifautoCompactionRetention has been raised far enough that the retained history outgrows 2 GiB.
The symptom usually surfaces in the client rather than in etcd. A Patroni replica that cannot renew its lease exits cleanly, so it restart-loops with exitCode: 0 and reason: Completed and a climbing restart count — which reads as healthy and gets misdiagnosed as a database fault. Check etcd first.
Both inspection commands are read-only and safe to run on a live cluster:
DB SIZE close to 2.1 GB on every member, plus NOSPACE in the alarm list, confirms it. A healthy cluster prints nothing at all for alarm list. Recovery from there — compacting to a revision, defragmenting each member, then disarming the alarm — is an operator procedure this template deliberately does not perform, because each step is disruptive and the order matters. Follow etcd’s maintenance guide and plan it as a maintenance window.
Troubleshooting
When a member will not come up, its own[etcd] log lines name the reason:
Install fails: etcd-multi-location requires at least 2 locations in global.locations
Install fails: etcd-multi-location requires at least 2 locations in global.locations
global.locations at render time and rejects a list with fewer than two entries before any resource is created. This also happens when a --set global.locations[0].name=... override replaces the whole list with a single entry.Fix: Supply the full list in a values file (see Installation). For a single-location cluster use the etcd template.Install or upgrade fails: the global.gvc values key was REMOVED
Install or upgrade fails: the global.gvc values key was REMOVED
global.gvc block. Version 2.0.0 no longer creates a GVC and refuses to render rather than let an in-place upgrade delete the one the old release created.Fix: Do not upgrade in place. Follow Migrating From Version 1: install 2.0.0 as a new release with global.locations, move the keyspace, then uninstall the old release.Member exits with [etcd] FATAL: locations declared in global.locations are not in GVC
Member exits with [etcd] FATAL: locations declared in global.locations are not in GVC
1; its log names the missing location(s) and reads That is a bare majority: the cluster would form, report healthy, and have ZERO fault tolerance.Cause: global.locations names a location the GVC does not have. The members in the other locations would still form a cluster, but one with no fault tolerance, so a fresh member refuses to bootstrap it. An already-initialized member logs the same condition as a [etcd] WARNING and keeps serving.Fix: Add the missing location to the GVC, or remove it from global.locations, and redeploy. cpln gvc get GVC_NAME -o yaml shows the GVC’s list under spec.staticPlacement.locationLinks.Member exits with [etcd] FATAL: quorum is unreachable
Member exits with [etcd] FATAL: quorum is unreachable
global.locations exist in the GVC, so an election can never complete. A fresh member exits immediately rather than waiting forever; an already-initialized member logs a [etcd] WARNING and keeps running.Fix: Add the missing locations to the GVC or remove them from global.locations so that the GVC has a majority of the declared list — ideally all of it — and redeploy.Member exits with [etcd] FATAL: this member is running in location ..., which is not in the configured location list
Member exits with [etcd] FATAL: this member is running in location ..., which is not in the configured location list
global.locations does not list. Such a member can never appear in the cluster roster, so it exits rather than wait to join.Fix: Either add that location to global.locations and upgrade, or remove it from the GVC. Normally an unlisted GVC location runs nothing at all; this line only appears if a replica was placed there anyway.Log shows [etcd] WARNING: could not read the location list of GVC
Log shows [etcd] WARNING: could not read the location list of GVC
RELEASE_NAME-etcd-gvc-policy policy is missing — so it skipped the location check and started anyway. The warning is informational; etcd runs normally.Fix: Nothing, if the policy exists and the warning was transient. If it recurs on every start, confirm the policy with cpln policy get RELEASE_NAME-etcd-gvc-policy -o yaml and that it grants view on the GVC the release is installed into.etcdctl hangs, then reports context deadline exceeded
etcdctl hangs, then reports context deadline exceeded
--consistency=s) keep answering from stale local data.Fix: Check which members are healthy and whether any replica is restarting:--command-timeout so an outage surfaces as an error instead of a hang.endpoint status shows IS LEADER: true but writes still hang
endpoint status shows IS LEADER: true but writes still hang
IS LEADER is the member’s own view of itself. A member cut off from the others can keep reporting itself as leader for a short while after it has lost the ability to commit anything.Fix: Health-check with a linearizable read (etcdctl --command-timeout=5s get KEY, without --consistency=s), never with endpoint status alone. Then check etcdctl endpoint health --cluster and the deployment status as above.Recovery appears to do nothing after setting recovery.forceNewClusterInLocation
Recovery appears to do nothing after setting recovery.forceNewClusterInLocation
[etcd] WARNING: recovery.forceNewClusterInLocation is set to '...', which GVC '...' does not have.Cause: The value passed render-time validation because it is in global.locations, but the GVC has no such location, so no member runs there and --force-new-cluster is never applied.Fix: Set recovery.forceNewClusterInLocation to a location the GVC actually has — the one whose member survived — and upgrade again.Writes fail with etcdserver: mvcc: database space exceeded
Writes fail with etcdserver: mvcc: database space exceeded
NOSPACE alarm. Every member is read-only until the alarm is cleared.Fix: Confirm with etcdctl alarm list, then follow If the Backend Quota Is Already Full. Upgrading the template alone does not clear it.Reads fail with etcdserver: mvcc: required revision has been compacted
Reads fail with etcdserver: mvcc: required revision has been compacted
get --rev or a watch resuming from a revision that auto-compaction has already discarded. With the default autoCompactionRetention: 1h only the last hour of history is readable.Fix: Read the current revision instead, or restart the watch from the current revision. If your application genuinely needs a longer history, raise tuning.autoCompactionRetention (for example 24h) and accept the larger backend.Patroni replicas restart-loop with exitCode 0 and reason Completed
Patroni replicas restart-loop with exitCode 0 and reason Completed
etcdctl endpoint health --cluster before touching the database.Important Notes
- Do not
cpln helm upgradea 1.x release onto this version. It deletes the GVC the old release created and everything inside it. Follow Migrating From Version 1. global.locationsmust match the GVC’s locations. A declared location the GVC lacks makes a fresh member refuse to start with a named[etcd] FATALline; a GVC location you did not list runs nothing. Read the GVC’s list withcpln gvc get GVC_NAME -o yamlbefore installing.- Quorum arithmetic decides your failure tolerance. Two locations survive zero losses. If you want automatic failover, use three.
- Every
cpln helm upgraderestarts the members together, including a no-op one. Expect a brief write outage; the data survives. Plan upgrades as a short planned outage. - Changing
global.locationsreprovisions the cluster. Plan it as a maintenance window and change the GVC’s locations together with the list. - Under quorum loss, writes hang rather than failing fast, and serializable reads keep succeeding against stale data. Give clients a short command timeout and health-check with a linearizable read, never with
IS LEADERalone. - No TLS, no authentication. Scope
internalAccesswithworkload-listwhenever the GVC holds workloads that should not read or write the keyspace; list only your clients. - Compaction is required and cannot be switched off. A full backend quota puts the cluster into a read-only
NOSPACEalarm that an upgrade cannot clear. recovery.forceNewClusterInLocationis for a permanent loss of quorum only. Never set it to more than one location, and clear it as soon as the cluster is back.- Uninstalling deletes the volume sets, and with them the entire keyspace. The volume set takes a final snapshot first.