etcd Cluster Installation and Management
etcd is a distributed key-value store that uses the Raft consensus algorithm to provide strong consistency, making it the backbone of Kubernetes and other distributed systems. This guide covers deploying a production etcd cluster on Linux with TLS security, backup and restore procedures, and performance tuning.
Prerequisites
- 3 Linux servers (odd number required) running Ubuntu 22.04 or CentOS/Rocky 9
- At minimum 2 CPU cores and 4 GB RAM per node; SSD storage strongly recommended
- All nodes can reach each other on TCP ports 2379 (client) and 2380 (peer)
- Static IP addresses and synchronized clocks (NTP)
cfssloropensslfor certificate generation
Install etcd
Install the same version on all 3 nodes:
# Download the latest stable release
ETCD_VER=v3.5.12
DOWNLOAD_URL=https://github.com/etcd-io/etcd/releases/download
curl -L ${DOWNLOAD_URL}/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz \
-o /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
tar xvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C /tmp/
sudo mv /tmp/etcd-${ETCD_VER}-linux-amd64/etcd* /usr/local/bin/
# Verify
etcd --version
etcdctl version
Create the system user and directories:
sudo useradd -r -s /sbin/nologin etcd
sudo mkdir -p /etc/etcd /var/lib/etcd
sudo chown etcd:etcd /var/lib/etcd
Generate TLS Certificates
Generate a CA and certificates for peer and client communication:
# Install cfssl (run on one node, distribute certs)
curl -L https://github.com/cloudflare/cfssl/releases/download/v1.6.4/cfssl_1.6.4_linux_amd64 \
-o /usr/local/bin/cfssl && chmod +x /usr/local/bin/cfssl
curl -L https://github.com/cloudflare/cfssl/releases/download/v1.6.4/cfssljson_1.6.4_linux_amd64 \
-o /usr/local/bin/cfssljson && chmod +x /usr/local/bin/cfssljson
mkdir -p ~/etcd-certs && cd ~/etcd-certs
# CA config
cat > ca-config.json << 'EOF'
{
"signing": {
"default": { "expiry": "8760h" },
"profiles": {
"etcd": {
"expiry": "8760h",
"usages": ["signing", "key encipherment", "server auth", "client auth"]
}
}
}
}
EOF
# CA CSR
cat > ca-csr.json << 'EOF'
{"CN":"etcd-ca","key":{"algo":"rsa","size":2048},"names":[{"C":"US","ST":"CA","L":"San Francisco","O":"etcd"}]}
EOF
cfssl gencert -initca ca-csr.json | cfssljson -bare ca
# Generate server cert covering all 3 node IPs and localhost
cat > etcd-csr.json << 'EOF'
{
"CN": "etcd",
"hosts": [
"localhost", "127.0.0.1",
"192.168.1.10", "192.168.1.11", "192.168.1.12",
"etcd-01", "etcd-02", "etcd-03"
],
"key": {"algo": "rsa", "size": 2048},
"names": [{"C":"US","ST":"CA","O":"etcd"}]
}
EOF
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem \
-config=ca-config.json -profile=etcd etcd-csr.json | cfssljson -bare etcd
# Copy certs to all nodes
for node in 192.168.1.10 192.168.1.11 192.168.1.12; do
scp ca.pem etcd.pem etcd-key.pem root@${node}:/etc/etcd/
done
sudo chmod 600 /etc/etcd/etcd-key.pem
sudo chown etcd:etcd /etc/etcd/*.pem
Bootstrap the etcd Cluster
Create the systemd service on each node. Replace ETCD_NAME and ETCD_INITIAL_ADVERTISE_PEER_URLS per node:
# On etcd-01 (192.168.1.10)
sudo tee /etc/systemd/system/etcd.service << 'EOF'
[Unit]
Description=etcd distributed key-value store
Documentation=https://github.com/etcd-io/etcd
After=network.target
[Service]
User=etcd
Type=notify
ExecStart=/usr/local/bin/etcd \
--name etcd-01 \
--data-dir /var/lib/etcd \
--listen-peer-urls https://192.168.1.10:2380 \
--listen-client-urls https://192.168.1.10:2379,https://127.0.0.1:2379 \
--advertise-client-urls https://192.168.1.10:2379 \
--initial-advertise-peer-urls https://192.168.1.10:2380 \
--initial-cluster etcd-01=https://192.168.1.10:2380,etcd-02=https://192.168.1.11:2380,etcd-03=https://192.168.1.12:2380 \
--initial-cluster-state new \
--initial-cluster-token etcd-cluster-01 \
--peer-cert-file=/etc/etcd/etcd.pem \
--peer-key-file=/etc/etcd/etcd-key.pem \
--peer-trusted-ca-file=/etc/etcd/ca.pem \
--peer-client-cert-auth=true \
--cert-file=/etc/etcd/etcd.pem \
--key-file=/etc/etcd/etcd-key.pem \
--trusted-ca-file=/etc/etcd/ca.pem \
--client-cert-auth=true \
--auto-compaction-retention=1 \
--quota-backend-bytes=8589934592
Restart=on-failure
RestartSec=5
LimitNOFILE=40000
[Install]
WantedBy=multi-user.target
EOF
# Start on all 3 nodes simultaneously
sudo systemctl daemon-reload
sudo systemctl enable --now etcd
Verify Cluster Health
# Set etcdctl environment variables
export ETCDCTL_API=3
export ETCDCTL_ENDPOINTS=https://192.168.1.10:2379,https://192.168.1.11:2379,https://192.168.1.12:2379
export ETCDCTL_CACERT=/etc/etcd/ca.pem
export ETCDCTL_CERT=/etc/etcd/etcd.pem
export ETCDCTL_KEY=/etc/etcd/etcd-key.pem
# Check cluster membership
etcdctl member list -w table
# Check endpoint health
etcdctl endpoint health -w table
# Check endpoint status (shows leader)
etcdctl endpoint status -w table
Working with etcd Data
# Write a key
etcdctl put /config/database/host "db.example.com"
etcdctl put /config/database/port "5432"
# Read a key
etcdctl get /config/database/host
# List all keys under a prefix
etcdctl get /config/ --prefix
# Delete a key
etcdctl del /config/database/port
# Watch for changes in real-time
etcdctl watch /config/ --prefix &
# Write with TTL (lease)
etcdctl lease grant 120 # 120 second TTL, returns lease ID
etcdctl put /locks/job1 "worker-01" --lease=<lease-id>
etcdctl lease keep-alive <lease-id> # renew the lease
Backup and Restore
Take regular snapshots for disaster recovery:
# Create a snapshot backup
etcdctl snapshot save /backup/etcd-snapshot-$(date +%Y%m%d-%H%M%S).db
# Verify the snapshot
etcdctl snapshot status /backup/etcd-snapshot-20260101-120000.db -w table
# Automate backups with cron
cat > /usr/local/bin/etcd-backup.sh << 'EOF'
#!/bin/bash
BACKUP_DIR=/backup/etcd
SNAPSHOT_FILE=${BACKUP_DIR}/snapshot-$(date +%Y%m%d-%H%M%S).db
export ETCDCTL_API=3
export ETCDCTL_ENDPOINTS=https://127.0.0.1:2379
export ETCDCTL_CACERT=/etc/etcd/ca.pem
export ETCDCTL_CERT=/etc/etcd/etcd.pem
export ETCDCTL_KEY=/etc/etcd/etcd-key.pem
mkdir -p ${BACKUP_DIR}
etcdctl snapshot save ${SNAPSHOT_FILE}
# Keep only last 7 days of backups
find ${BACKUP_DIR} -name "snapshot-*.db" -mtime +7 -delete
echo "Backup saved: ${SNAPSHOT_FILE}"
EOF
chmod +x /usr/local/bin/etcd-backup.sh
# Add to crontab
echo "0 2 * * * root /usr/local/bin/etcd-backup.sh" | sudo tee /etc/cron.d/etcd-backup
Restore from snapshot:
# Stop etcd on ALL nodes first
sudo systemctl stop etcd
# Restore on each node (use the same snapshot, different data dirs if clustered)
etcdctl snapshot restore /backup/etcd-snapshot-20260101-120000.db \
--name etcd-01 \
--initial-cluster etcd-01=https://192.168.1.10:2380,etcd-02=https://192.168.1.11:2380,etcd-03=https://192.168.1.12:2380 \
--initial-cluster-token etcd-cluster-restored \
--initial-advertise-peer-urls https://192.168.1.10:2380 \
--data-dir /var/lib/etcd-restored
sudo mv /var/lib/etcd /var/lib/etcd-old
sudo mv /var/lib/etcd-restored /var/lib/etcd
sudo chown -R etcd:etcd /var/lib/etcd
# Start etcd after restoring all nodes
sudo systemctl start etcd
Monitoring and Performance Tuning
# Check metrics endpoint (Prometheus format)
curl -s https://127.0.0.1:2379/metrics \
--cacert /etc/etcd/ca.pem \
--cert /etc/etcd/etcd.pem \
--key /etc/etcd/etcd-key.pem | grep etcd_server
# Compact old revisions to reclaim disk space
# First find the current revision
REV=$(etcdctl endpoint status --write-out="json" | python3 -c \
"import sys,json; print(json.load(sys.stdin)[0]['Status']['header']['revision'])")
# Compact to current revision
etcdctl compact $REV
# Defragment to reclaim disk space (one member at a time)
etcdctl defrag --endpoints=https://192.168.1.10:2379
etcdctl defrag --endpoints=https://192.168.1.11:2379
etcdctl defrag --endpoints=https://192.168.1.12:2379
Key performance settings in the service file:
# Increase heartbeat for high-latency networks (default: 100ms)
--heartbeat-interval=250
--election-timeout=1250
# Set backend quota (default 2GiB, max 8GiB)
--quota-backend-bytes=8589934592
# Auto-compact every hour
--auto-compaction-retention=1
--auto-compaction-mode=periodic
Troubleshooting
Cluster won't bootstrap - peer connection refused:
# Check firewall rules
sudo firewall-cmd --list-all # CentOS/Rocky
sudo ufw status # Ubuntu
# Allow etcd ports
sudo ufw allow 2379/tcp
sudo ufw allow 2380/tcp
# Check that all nodes can reach each other
nc -zv 192.168.1.11 2380
"no leader" or split-brain:
# Check quorum - with 3 nodes you can lose 1
etcdctl endpoint status -w table
# If majority of nodes are down, check logs
journalctl -u etcd -n 100 --no-pager
Database too large / quota exceeded:
# Check current database size
etcdctl endpoint status -w table
# Compact and defrag immediately
etcdctl compact $(etcdctl endpoint status --write-out="json" | \
python3 -c "import sys,json; print(json.load(sys.stdin)[0]['Status']['header']['revision'])")
etcdctl defrag
Conclusion
A production etcd cluster requires TLS encryption, regular snapshots, and disk performance monitoring to remain reliable under load. Running 3 nodes with Raft consensus tolerates one node failure while maintaining availability. Always take a snapshot before major operations, automate backups with cron, and monitor the backend database size to prevent quota-exceeded errors that block all writes.


