Etcd
etcd is a distributed key-value store. pgcli can run one or more etcd members as a standalone, top-level addon — shared infrastructure rather than a per-instance sidecar. This is useful as the DCS (Distributed Concurrent Store) backing a PostgreSQL HA stack, or as a general config/lock service.
Security caveat: pgcli-managed etcd members currently run without TLS/CA certificates and without authentication/RBAC — any client that can reach a client port has full read/write access. Plan a deployment around network isolation (loopback binds by default; keep advertised ports inside a trusted network). CA/TLS and auth/RBAC support is on the roadmap for a later release.
Members are managed one pg addon install etcd at a time: the first member
bootstraps a cluster, and later members join the same named cluster on the fly
— from the same host or from another machine.
How It Works
- Shared infrastructure: etcd lives in the top-level
addons.etcdmap inpg.yaml, keyed by member name — not under any single instance. - Host network, configurable bind: each member runs with
--network host. By default its client/peer URLs bind to127.0.0.1— etcd ships without authentication, so loopback-only exposure is the intended posture for a single-host cluster. For cross-host clusters each member advertises a reachable address (--advertise-host) while also listening on loopback, so localetcdctland remote peers both work. - Dynamic membership: the first member starts with
--initial-cluster-state new; each subsequent member is registered withetcdctl member addagainst a running peer, then started with--initial-cluster-state existing. pgcli does this automatically — same-host via a member container, cross-host via a temporary etcdctl container. - Cluster identity: members sharing the same
--clustervalue join the same etcd cluster (it maps to etcd’s--initial-cluster-token). The default ispgcli-etcd. - Baked-in tuning: every member launches with periodic compaction
(
--auto-compaction-mode periodic,--auto-compaction-retention 24h) and an 8 GiB backend quota (--quota-backend-bytes 8589934592) — sane defaults for a small HA metadata store.
Deployment Topologies
pgcli supports two cluster shapes. Both use the same install commands — the difference is whether members share a host.
Single host — all members on one machine (testing / dev)
The classic layout for a dev box or CI: every member listens on loopback with a
different auto-assigned port. No --advertise-host needed (default
127.0.0.1), no firewall changes, nothing reachable from outside the host.
All three belong to cluster pgcli-etcd; m1 bootstraps, m2/m3 join on the fly.
This gives you real 3-node raft semantics but zero host isolation — the whole
cluster dies with one machine, which is exactly why it is a testing topology.
Cross host — one member per machine (production HA)
The production shape: spread members across machines (ideally 3 or 5, an odd
count — see Topology & Quorum). Each member advertises its host’s LAN address;
the first member must bootstrap with --advertise-host so remote peers can
dial it. Ports may repeat on every host since each binds its own interface.
Requirements: every member’s --advertise-host is a mutual-LAN-reachable
address, the firewall on each host lets the other members through on every
member’s client and peer ports (bidirectional peer traffic — e.g. open
2379-2386/tcp for a 4-member cluster on default ports), and all members share
the same --cluster token. Each host survives losing any one member; the
cluster only needs a majority of machines.
| Single host | Cross host | |
|---|---|---|
| Use for | dev, CI, functional testing | production HA |
--advertise-host |
omit (loopback default) | required, every member |
--join |
not used | every member after the first |
| Ports | unique per member on one host | may repeat, one host each |
| Firewall | none (loopback only) | open every member’s client+peer ports, both ways |
| Survives machine loss | no | yes (with quorum) |
Commands
Create the first member
The client port starts at 2379 and the peer port takes the next free port
(2380), auto-allocated from etcd_start_port.
Add members to the same cluster
With m1 running, installing another member of the same --cluster joins it:
pgcli finds a running peer to act as coordinator, registers m2/m3 via
etcdctl member add, and starts them with --initial-cluster-state existing.
Ports continue to auto-assign without collisions (m2 → 2381/2382, m3 →
2383/2384). A freshly-grown cluster briefly lacks quorum during leader
re-election; pgcli retries the membership operations automatically, so you do
not need to wait between installs.
Use a different --cluster to keep members in a separate etcd cluster.
Join a member on another machine
Members on different hosts form one cluster too. Each cross-host member must
advertise an address the other members can reach, set with
--advertise-host. On the machine that already hosts a member, bootstrap as
usual, passing its LAN address:
Then on the new host, point --join at any existing member’s client endpoint.
pgcli registers the member through that endpoint (from a temporary etcdctl
container — no local member container or image needed; the image is pulled on
demand), takes the authoritative ETCD_INITIAL_CLUSTER from the response, and
starts the member:
Notes for cross-host clusters:
- Every member needs a reachable
--advertise-host— including the first. Its advertised peer URL propagates into the cluster’s membership list, so a first member started on the default127.0.0.1can never be joined from another host. If you plan a cross-host cluster, pass the LAN address at bootstrap time. --clusteris required with--joinand must match the remote cluster’s token — membership is checked by that cluster, not by the local config.--advertise-hostis required with--join; without it the other members could register a peer URL they can’t dial.- Ports may repeat across hosts (
2379/2380on both) since each host binds its own interfaces. - A member’s client/peer ports must be reachable between hosts — open your
firewall for every member’s ports, in both directions. Peers dial each
other’s peer ports bidirectionally, and clients (and
--join/pg etcdctl) reach the client ports, so allow both. Auto-assignment means the exact numbers vary per member — read them from the install summary orpg addon list(or pin them with--client-port/--peer-port) and open that range, e.g.2379-2386/tcpfor a 4-member cluster on default ports. Skipping this is the usual cause of a member hanging withetcdserver: no leaderor a--jointhat times out. - Re-running the same
--joininstall for an already-registered name fails with a clear error; deregister first viapg etcdctl member remove <hex-id>against the cluster.
Inspect with pg etcdctl
pg etcdctl runs etcd’s client from a short-lived container — no need to
exec into a member (and it works even on a host with no member
installed, pulling the image on demand). The target comes from
ETCDCTL_ENDPOINTS, falling back to the first configured member:
etcdctl flags that pg’s own parser would reject (-w table, --hex, …) go
after --.
List
etcd members appear under the Infra add-ons (etcd) section:
Start and stop
After a host reboot or a manual stop, bring a member back up without
re-running install — no member add, the existing config and on-disk data are
reused:
start is safe to repeat: an already-running member is a no-op, and a member
whose container was removed is recreated from its data with
--initial-cluster-state existing. For a cross-host cluster, confirm the
member’s peers are reachable (quorum intact) before starting it.
Remove a member
Removal first deregisters the member from the cluster (resolving its hex
member ID — etcd v3.5 member remove takes an ID, not a name), then deletes
the container and the member’s data directory. Remaining members stay healthy
as long as quorum holds.
Cross-host members must be deregistered separately. The automatic
deregistration above only works when another running member of the same
cluster is present in the local pg.yaml — pgcli has no view of members that
live on other hosts. So on a cross-host cluster, removing a member from the
host that runs it deletes the container and data but leaves its entry in the
cluster’s membership list (a “stale” member) — the surviving members keep
trying to peer with the now-deleted host.
Remove it in two steps instead:
pg etcdctl needs a reachable peer to talk to — point ETCDCTL_ENDPOINTS at
a member of that cluster that is still running (on a host that still has a
running member, this falls back automatically; otherwise set it explicitly),
then remove by ID (see Inspect with pg etcdctl).
Verify with pg etcdctl member list afterward: the removed name should be gone.
Parameters
| Flag | Description | Default |
|---|---|---|
--name |
Member name, and the pg.yaml config key |
etcd |
--cluster |
Cluster identity (--initial-cluster-token); same value = same cluster |
pgcli-etcd |
--client-port |
Client host port (0 = auto-assign from etcd_start_port) |
auto |
--peer-port |
Peer host port (0 = auto-assign, next free port) | auto |
--image |
etcd image tag | quay.io/coreos/etcd:v3.5.30 |
--data-dir |
Data dir root — absolute, or relative to base_dir; each member uses <root>/<name>/data |
<base_dir>/addon/etcd |
--advertise-host |
Host advertised in this member’s peer/client URLs (empty = 127.0.0.1 for single-host; set a LAN IP or FQDN for cross-host) |
127.0.0.1 |
--join |
Client endpoint of an existing member to join cross-host, e.g. http://10.0.0.1:2379 (implies --initial-cluster-state existing; requires --advertise-host and --cluster) |
— |
Container name follows the namespace convention:
pgcli-etcd-<namespace>-<name> (namespace omitted when unset).
Data Directory Layout
Data is always laid out as <root>/<name>/data, so multiple members on one
host never share a directory:
--data-dir sets the root. A relative value resolves against the config’s
base_dir; the resolved root is persisted, so the config stays portable:
pg addon remove deletes only that member’s <name>/data dir and leaves the
shared root in place.
Connecting
Use pg etcdctl, which runs etcd’s v3 client from a short-lived container
against a member’s client URL — the target comes from ETCDCTL_ENDPOINTS,
falling back to the first configured member. etcdctl’s native flags go after
--:
pg etcdctl always speaks the v3 API (ETCDCTL_API=3, etcdctl’s default); v2
is not supported.
Topology & Quorum
etcd requires a quorum (majority) of members to accept writes:
| Members | Quorum | Tolerates failures |
|---|---|---|
| 1 | 1 | 0 |
| 3 | 2 | 1 |
| 5 | 3 | 2 |
- Use an odd number of members — 3 or 5 for production HA.
- Removing members can drop the cluster below quorum (e.g. 3 → 1 surviving); it will then be unable to commit writes. Keep a majority running.
Configuration
After install, pg.yaml records each member under the top-level addons.etcd:
Port base is configurable via top-level etcd_start_port (default 2379). A
cross-host member records the address it advertises:
Auto-start on Boot
Containers also carry a --restart unless-stopped policy, which a per-container
conmon monitor enforces even without a podman daemon — it covers crashes but
not host reboots. To bring members up after a reboot, enable autostart
per member:
This sets autostart: true on the member in pg.yaml and installs/refreshes
the boot service (see Auto-start on Boot). Boot is
start-only: it starts the member’s existing container and never re-runs
member add — an initialized member reloads its cluster from disk and rejoins.
On a cross-host cluster, run the command on each host for that host’s
member(s). pg autostart status lists every member’s state.
Notes
- Single-node vs cluster:
--name m1alone gives a one-member cluster; install more members with the same--clusterto grow it. - Production-ready defaults: every member already launches with periodic compaction and an 8 GiB backend quota (see How It Works), suitable for a production DCS without further tuning.
- Reinstall is idempotent: re-running
pg addon install etcd --name <m>on a running member recreates its container with updated flags; a member already registered in the cluster is not re-added.