Introduction
etcd is the distributed key-value store that underpins Kubernetes — storing all cluster state, configuration, and service discovery data. Ansible automates etcd cluster deployment: static bootstrapping, TLS mutual authentication, automated backups, member management, and health monitoring.
Deploy etcd Cluster
---
- name: Deploy etcd cluster
hosts: etcd_nodes
become: true
vars:
etcd_version: "3.5.15"
etcd_data_dir: /var/lib/etcd
etcd_client_port: 2379
etcd_peer_port: 2380
etcd_cluster_token: my-etcd-cluster
tasks:
- name: Create etcd user
ansible.builtin.user:
name: etcd
system: true
shell: /usr/sbin/nologin
home: "{{ etcd_data_dir }}"
- name: Download etcd
ansible.builtin.get_url:
url: "https://github.com/etcd-io/etcd/releases/download/v{{ etcd_version }}/etcd-v{{ etcd_version }}-linux-amd64.tar.gz"
dest: /tmp/etcd.tar.gz
- name: Extract etcd
ansible.builtin.unarchive:
src: /tmp/etcd.tar.gz
dest: /tmp/
remote_src: true
- name: Install binaries
ansible.builtin.copy:
src: "/tmp/etcd-v{{ etcd_version }}-linux-amd64/{{ item }}"
dest: "/usr/local/bin/{{ item }}"
remote_src: true
mode: '0755'
loop: [etcd, etcdctl, etcdutl]
- name: Create data directory
ansible.builtin.file:
path: "{{ etcd_data_dir }}"
state: directory
owner: etcd
mode: '0700'
- name: Deploy etcd config
ansible.builtin.template:
src: etcd.conf.yml.j2
dest: /etc/etcd/etcd.conf.yml
owner: etcd
mode: '0640'
notify: restart etcd
- name: Create systemd service
ansible.builtin.copy:
dest: /etc/systemd/system/etcd.service
content: |
[Unit]
Description=etcd
After=network-online.target
[Service]
User=etcd
Type=notify
ExecStart=/usr/local/bin/etcd --config-file /etc/etcd/etcd.conf.yml
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
mode: '0644'
notify:
- daemon reload
- restart etcd
- name: Allow etcd through firewall
ansible.posix.firewalld:
port: "{{ item }}/tcp"
permanent: true
state: enabled
immediate: true
loop:
- "{{ etcd_client_port }}"
- "{{ etcd_peer_port }}"
- name: Start etcd
ansible.builtin.service:
name: etcd
state: started
enabled: true
handlers:
- name: daemon reload
ansible.builtin.systemd:
daemon_reload: true
- name: restart etcd
ansible.builtin.service:
name: etcd
state: restarted
Config Template
# templates/etcd.conf.yml.j2
name: {{ inventory_hostname }}
data-dir: {{ etcd_data_dir }}
initial-advertise-peer-urls: https://{{ ansible_default_ipv4.address }}:{{ etcd_peer_port }}
listen-peer-urls: https://{{ ansible_default_ipv4.address }}:{{ etcd_peer_port }}
advertise-client-urls: https://{{ ansible_default_ipv4.address }}:{{ etcd_client_port }}
listen-client-urls: https://{{ ansible_default_ipv4.address }}:{{ etcd_client_port }},https://127.0.0.1:{{ etcd_client_port }}
initial-cluster-token: {{ etcd_cluster_token }}
initial-cluster: {% for host in groups['etcd_nodes'] %}{{ host }}=https://{{ hostvars[host].ansible_default_ipv4.address }}:{{ etcd_peer_port }}{% if not loop.last %},{% endif %}{% endfor %}
initial-cluster-state: new
{% if etcd_tls_enabled | default(true) %}
client-transport-security:
cert-file: /etc/etcd/ssl/server.crt
key-file: /etc/etcd/ssl/server.key
client-cert-auth: true
trusted-ca-file: /etc/etcd/ssl/ca.crt
peer-transport-security:
cert-file: /etc/etcd/ssl/peer.crt
key-file: /etc/etcd/ssl/peer.key
client-cert-auth: true
trusted-ca-file: /etc/etcd/ssl/ca.crt
{% endif %}
auto-compaction-retention: "8"
auto-compaction-mode: periodic
snapshot-count: 10000
TLS Certificates
- name: Deploy etcd TLS certificates
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: etcd
mode: "{{ item.mode }}"
loop:
- { src: "ssl/ca.crt", dest: "/etc/etcd/ssl/ca.crt", mode: "0644" }
- { src: "ssl/{{ inventory_hostname }}.crt", dest: "/etc/etcd/ssl/server.crt", mode: "0644" }
- { src: "ssl/{{ inventory_hostname }}.key", dest: "/etc/etcd/ssl/server.key", mode: "0600" }
- { src: "ssl/{{ inventory_hostname }}-peer.crt", dest: "/etc/etcd/ssl/peer.crt", mode: "0644" }
- { src: "ssl/{{ inventory_hostname }}-peer.key", dest: "/etc/etcd/ssl/peer.key", mode: "0600" }
notify: restart etcd
Automated Backups
- name: Deploy etcd backup script
ansible.builtin.copy:
dest: /usr/local/bin/etcd-backup.sh
content: |
#!/bin/bash
BACKUP_DIR=/var/backups/etcd
RETENTION_DAYS=7
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
mkdir -p $BACKUP_DIR
ETCDCTL_API=3 etcdctl snapshot save \
$BACKUP_DIR/snapshot-$TIMESTAMP.db \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/etcd/ssl/ca.crt \
--cert=/etc/etcd/ssl/server.crt \
--key=/etc/etcd/ssl/server.key
# Verify backup
ETCDCTL_API=3 etcdctl snapshot status \
$BACKUP_DIR/snapshot-$TIMESTAMP.db --write-out=table
# Clean old backups
find $BACKUP_DIR -name "snapshot-*.db" -mtime +$RETENTION_DAYS -delete
mode: '0755'
- name: Schedule daily backup
ansible.builtin.cron:
name: "etcd backup"
minute: "0"
hour: "2"
job: "/usr/local/bin/etcd-backup.sh >> /var/log/etcd-backup.log 2>&1"
user: etcd
Health Check
- name: Check etcd cluster health
ansible.builtin.command: >
etcdctl endpoint health
--endpoints=https://{{ ansible_default_ipv4.address }}:{{ etcd_client_port }}
--cacert=/etc/etcd/ssl/ca.crt
--cert=/etc/etcd/ssl/server.crt
--key=/etc/etcd/ssl/server.key
register: health
changed_when: false
environment:
ETCDCTL_API: "3"
- name: Check member list
ansible.builtin.command: >
etcdctl member list
--endpoints=https://127.0.0.1:{{ etcd_client_port }}
--cacert=/etc/etcd/ssl/ca.crt
--cert=/etc/etcd/ssl/server.crt
--key=/etc/etcd/ssl/server.key
--write-out=table
register: members
changed_when: false
run_once: true
environment:
ETCDCTL_API: "3"
Restore from Backup
- name: Restore etcd from snapshot
ansible.builtin.command: >
etcdctl snapshot restore {{ backup_file }}
--data-dir={{ etcd_data_dir }}-restored
--name={{ inventory_hostname }}
--initial-cluster={{ initial_cluster }}
--initial-advertise-peer-urls=https://{{ ansible_default_ipv4.address }}:{{ etcd_peer_port }}
environment:
ETCDCTL_API: "3"
when: restore_etcd | default(false)
Troubleshooting
Database Too Large
- name: Compact and defragment
ansible.builtin.shell: |
etcdctl compact $(etcdctl endpoint status --write-out=json | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['Status']['header']['revision'])")
etcdctl defrag
environment:
ETCDCTL_API: "3"
ETCDCTL_ENDPOINTS: "https://127.0.0.1:{{ etcd_client_port }}"
ETCDCTL_CACERT: "/etc/etcd/ssl/ca.crt"
ETCDCTL_CERT: "/etc/etcd/ssl/server.crt"
ETCDCTL_KEY: "/etc/etcd/ssl/server.key"
run_once: true
changed_when: true
Related Articles
Conclusion
etcd is the backbone of Kubernetes and many distributed systems. Ansible deploys clusters with static bootstrapping, configures mutual TLS, schedules automated backups, and monitors cluster health. Template the config from inventory — the initial-cluster list is built dynamically from the etcd_nodes group. Use automated snapshots and tested restore procedures to ensure data durability.