Overview
etcd is a strongly consistent key-value store used as the coordination layer for leader election, distributed locking, and service 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. It is also the consensus store used by thepostgres-multi-location template, and is independently useful as a cross-region coordination store for your own services.
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 single-location cluster, use the etcd template instead.
What Gets Created
- Stateful etcd Workload — (
RELEASE_NAME-etcd): one member in each configured location, usingreplicaDirectaddressing so every member is individually reachable. Client API on2379, raft peer traffic on2380. - Volume Set — (
RELEASE_NAME-etcd-vs): persistent storage per member at/var/lib/etcdfor the raft write-ahead log and snapshots. ext4, general-purpose-ssd, final snapshot on delete, 7-day snapshot retention. - Secret — (
RELEASE_NAME-etcd-startup): an opaque startup script that computes each member’s name, peer URL, and the full cluster list at container start from the location it is running in. - Identity & Two Policies — An identity bound to the workload, granted
revealon the startup script secret and nothing else, plusviewon exactly the one GVC this release installs into. The second grant is what lets each member read its own GVC’s location list at boot; it is scoped to that single GVC, never to the whole org.
replica-0.RELEASE_NAME-etcd.LOCATION.GVC_NAME.cpln.local:2380). There is no operator, no discovery service, and no join step — every member receives the full cluster list up front and they elect a leader among themselves.
Installation
The only prerequisite is an existing GVC whose locations include every entry inglobal.locations — read its list with cpln gvc get GVC_NAME -o yaml and compare spec.staticPlacement.locationLinks before you install. Nothing else is needed: no cloud account, no bucket, no pre-created secret, no custom domain.
To install, follow the instructions for your preferred method:
UI
Browse, install, and manage templates visually
CLI
Manage templates from your terminal
Terraform
Declare templates in your Terraform configurations
Pulumi
Declare templates in your Pulumi programs
Configuration
The defaultvalues.yaml for this template:
Locations and Quorum
global.locations is the member map: one etcd member is deployed in each listed location, and at least 2 locations are required. Every name in it must already be one of the locations of the GVC you install into. The default list is the smallest cluster the chart allows rather than a recommendation — no built-in list can match your GVC, so set it before installing.
The list lives under global so a parent chart — postgres-multi-location consumes this template as a subchart — sets it once and Helm propagates it here. The two lists can then never be edited apart. Each location runs exactly one member; replicas is fixed at 1 and any other value fails a standalone render.
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:
With 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.
Changing
global.locations 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.Matching the Location List to the GVC
The platform does not validate the pairing in either direction, and both mismatches are dangerous in different ways. The template closes both.
Each member checks its own GVC at boot, using the scoped
view grant described in What Gets Created, and the check is deliberately asymmetric:
- On a fresh data directory, any declared location the GVC lacks is a hard failure. The container exits with code
1and logs a[etcd] FATAL:line naming the missing location and how many declared members could actually start. - On an already-initialized member it is a warning only, and the member keeps serving. A location removed from a GVC is indistinguishable from a location that is down, so failing here would turn “lost one member” into “lost the cluster”.
- If the GVC read itself fails — a control-plane blip, or a missing policy — the check is skipped with a warning. It never blocks a start on its own unavailability.
[etcd] lines in cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-etcd"}' when a member will not come up.
Image and Resources
image— The etcd image to run (etcd 3.6.5).resources.cpu/resources.memory— CPU and memory allocated to each member (default:500m,512Mi).
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 before campaigning. Must be at least 10× the heartbeat interval, and at most50000. Both bounds are enforced at render time.
Storage
volumeset.capacity— Persistent volume size in GiB per member for the raft write-ahead log and snapshots (minimum 10).
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 roughly every 10 seconds, which produces new revisions whether or not any application data changes. Measured on an otherwise idle cluster: about 151,000 revisions and 19 MB per day, which reaches etcd’s default 2 GiB backend quota in roughly 110 days. 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. It has always been enabled in this template; since version 1.0.2 it is also configurable, alongside the backend quota:
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 and is the value this template is tested at. 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. Each one produces a chart that installs cleanly and fails weeks later.
Compaction frees pages for reuse inside the backend file; it does not shrink the file. The reported
dbSize therefore plateaus rather than dropping, while dbSizeInUse falls back to the size of the live keyspace. That is the expected behavior 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.Testing measured the mechanism directly on a shortened retention window: the compacted revision advanced from 1 to 4001, dbSizeInUse collapsed from 5.76 MB to 20,480 bytes while dbSize stayed at about 5.8 MB, and reads of pre-compaction revisions then failed with required revision has been compacted. On this template’s own three-location run at the default 1h, compaction fired at the one-hour mark, freed 569 KB, and dbSize stayed flat afterwards under load. The multi-day dbSize plateau is derived from that mechanism rather than observed over days.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.Internal Access
TheinternalAccess section controls which workloads can reach the client API on 2379. Cross-location traffic inside one GVC is same-GVC traffic, so the default covers a stretched cluster with no extra rule.
A firewall change takes roughly 30 seconds to a few minutes to take effect — over two minutes was measured on this template, and other templates have taken longer still. Re-test after waiting rather than concluding the setting was ignored.
Connecting to etcd
GVC_NAME is the GVC you installed the release into — the template creates none of its own.
Point clients at one endpoint per location so they can fail over. From inside the cluster:
RELEASE_NAME-etcd-LOCATION, so member list maps one-to-one onto your location list.
Allow about two minutes of convergence after a cold install before concluding that a member is unreachable — cross-region service discovery can lag a workload reporting ready by well over a minute.
Ports
Failover Behavior
Measured on a three-location cluster (aws-us-east-1, aws-eu-central-1, aws-us-west-2) with the default 5 s election timeout:
Write latency is bounded by one cross-region round trip, because the leader needs a follower acknowledgement before it can commit. Measured with the leader in
aws-eu-central-1:
A client co-located with the leader does not get local-write latency — one cross-region round trip is the floor for any stretched quorum.
Upgrades
Nothing serializes the restart: the field that would limit it (rolloutOptions.maxUnavailableReplicas) is not retained by the platform, so the chart deliberately does not set it. Running more members per location has not been tested and should not be assumed to help.
Migrating From Version 1
Versions before 2.0.0 created their own GVC. Do nothelm upgrade a 1.x release onto 2.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. Measured on a sibling template: everything was gone in about six seconds, while the command printed upgraded successfully.
Install 2.0.0 as a new release against an existing GVC, move the keyspace across, then remove the old release.
1
Choose the target GVC
Pick a GVC whose locations are exactly the ones you want members in, and read them back with
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.2
Snapshot the old cluster's keyspace
3
Install 2.0.0 as a new release
Replace
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.4
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.5
Remove the old release
Uninstalling the old release removes it and the GVC it created, including its volume sets. Run it against the GVC the old release was installed into.
Behavior Under Quorum Loss
When a majority of members is unreachable, the cluster stops committing writes. What that looks like from a client is easy to misread:- Writes hang rather than failing fast. They block until they time out, so always give clients a short
--command-timeout(or the client-library equivalent) — otherwise connections pile up against a cluster that cannot commit. - Serializable reads keep succeeding against stale data. A read issued with
--consistency=sis served from the local member’s own store and never notices the loss of quorum. IS LEADER: trueis not proof of leadership. An isolated survivor keeps reporting itself as leader for about 6 seconds while unable to commit anything. Health-check with a linearizable read (etcdctl get KEY, without--consistency=s), never withendpoint status.
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 in well under a minute.
1
Confirm the loss is permanent
etcdctl member remove cannot help here, because removing a member itself requires quorum.2
Force a new single-member cluster
Set
recovery.forceNewClusterInLocation to the surviving location’s name and upgrade the release. That member restarts as a single-member cluster rebuilt from its own write-ahead log and serves writes again immediately.3
Clear the setting
Set
recovery.forceNewClusterInLocation back to "" and upgrade again. Leaving it set means the flag fires on every future restart of that member.4
Reset the lost location's volume before it rejoins
The evicted members still hold the old cluster ID and refuse to start until their data directory is cleared. Uninstall and reinstall, or delete that volume, so the member bootstraps fresh.
The render-time check only compares this value against
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 silently never applied. Every surviving member logs a warning saying exactly that — [etcd] WARNING: recovery.forceNewClusterInLocation is set to '...', which GVC '...' does not have — so read the logs if a recovery appears to do nothing.If the Backend Quota Fills
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:
--location LOCATION to target a specific location’s member; without it the CLI picks one for you and names it in its output. endpoint status --cluster reports every member from whichever one you reach.
A 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.
External References
etcd Documentation
Official etcd documentation
Maintenance and Compaction
Compaction, defragmentation, and clearing a NOSPACE alarm
Tuning for Latency
Heartbeat and election timeout guidance for cross-region clusters
Disaster Recovery
Recovering an etcd cluster that has lost quorum
etcd Multi-Location Template
View the source files, default values, and chart definition