Kubernetes the not so hard way with Ansible - etcd cluster (updated for K8s v1.11.x)

Install etcd cluster needed to store state of Kubernetes components

September 5, 2018



  • upgrade to etcd v3.2.18 (latest version supported/recommended for Kubernetes v1.11)
  • introduced etcd_ca_conf_directory variable to get more independed from other Kubernetes roles. In our case etcd_ca_conf_directory just points to the value of k8s_ca_conf_directory variable.

This post is based on Kelsey Hightower’s Kubernetes The Hard Way - Bootstrapping the etcd cluster.

In the previous part certificate authority we installed our PKI (public key infrastructure) in order to secure communication between our Kubernetes components/infrastructure. Now we use the certifcate authorities (CA) and generated keys for the first and very important component - the etcd cluster. etcd is basically a distributed key/value database. The Kubernetes components are stateless. All state is stored in etcd so you should take care of your etcd cluster in production. If you loose all etcd nodes you loose the whole Kubernetes state… So making a snapshot/backup from time to time is - at least - recommended ;-)

I want to mention that if your etcd nodes won’t join then a possible reason could be the certificate. If it isn’t your firewall blocking traffic between your etcd nodes the certifcate’s host list could be the problem. The error message isn’t always clear about the issue.

As usual we add the role ansible-role-etcd to the k8s.yml file e.g.:

  hosts: k8s_etcd
      role: githubixx.etcd
      tags: role-etcd

Next install the role via

ansible-galaxy install githubixx.etcd

(or just clone the Github repo whatever you like). Basically you don’t need to change a lot of variables but you can if you want of course:

# The directory from where to copy the etcd certificates.
etcd_ca_conf_directory: "{{k8s_ca_conf_directory}}"
# etcd version
etcd_version: "3.2.18"
# Port where etcd listening for clients
etcd_client_port: "2379"
# Port where etcd is listening for it's peer's
etcd_peer_port: "2380"
# Interface to bind etcd ports to
etcd_interface: "wg0"
# Directroy for etcd configuration
etcd_conf_dir: "/etc/etcd"
# Directory to store downloaded etcd archive
# Should not be deleted to avoid downloading over and over again
etcd_download_dir: "/opt/etcd"
# Directroy to store etcd binaries
etcd_bin_dir: "/usr/local/bin"
# etcd data directory (etcd database files so to say)
etcd_data_dir: "/var/lib/etcd"

# etcd flags/settings. This parameters are directly passed to "etcd" daemon during startup.
# To see all possible settings/flags either run "etcd --help" or have a look at the
# documentation. The dictionary keys below are just the flag names without "--". E.g.
# the first "name" flag below will be passed as "--name=whatever-hostname" to "etcd"
# daemon.
  "name": "{{ansible_hostname}}"
  "cert-file": "{{etcd_conf_dir}}/cert-etcd.pem"
  "key-file": "{{etcd_conf_dir}}/cert-etcd-key.pem"
  "peer-cert-file": "{{etcd_conf_dir}}/cert-etcd.pem"
  "peer-key-file": "{{etcd_conf_dir}}/cert-etcd-key.pem"
  "peer-trusted-ca-file": "{{etcd_conf_dir}}/ca-etcd.pem"
  "peer-client-cert-auth": "true" # # Enable peer client cert authentication
  "client-cert-auth": "true" # Enable client cert authentication
  "trusted-ca-file": "{{etcd_conf_dir}}/ca-etcd.pem"
  "advertise-client-urls": "{{'https://' + hostvars[inventory_hostname]['ansible_' + etcd_interface].ipv4.address + ':' + etcd_client_port}}"
  "initial-advertise-peer-urls": "{{'https://' + hostvars[inventory_hostname]['ansible_' + etcd_interface].ipv4.address + ':' + etcd_peer_port}}"
  "listen-peer-urls": "{{'https://' + hostvars[inventory_hostname]['ansible_' + etcd_interface].ipv4.address + ':' + etcd_peer_port}}"
  "listen-client-urls": "{{'https://' + hostvars[inventory_hostname]['ansible_' + etcd_interface].ipv4.address + ':' + etcd_client_port + ',' + etcd_client_port}}"
  "initial-cluster-token": "etcd-cluster-0" # Initial cluster token for the etcd cluster during bootstrap.
  "initial-cluster-state": "new" # Initial cluster state ('new' or 'existing')
  "data-dir": "{{etcd_data_dir}}" # etcd data directory (etcd database files so to say)
  "wal-dir": "" # Dedicated wal directory ("" means no seperated WAL directory)
  "auto-compaction-retention": "0" # Auto compaction retention in hour. 0 means disable auto compaction.
  "snapshot-count": "100000" # Number of committed transactions to trigger a snapshot to disk
  "heartbeat-interval": "100" # Time (in milliseconds) of a heartbeat interval
  "election-timeout": "1000" # Time (in milliseconds) for an election to timeout. See tuning documentation for details
  "max-snapshots": "5" # Maximum number of snapshot files to retain (0 is unlimited)
  "max-wals": "5" # Maximum number of wal files to retain (0 is unlimited)
  "cors": "" # Comma-separated whitelist of origins for CORS (cross-origin resource sharing)
  "quota-backend-bytes": "0" # Raise alarms when backend size exceeds the given quota (0 defaults to low space quota)
  "log-package-levels": "" # Specify a particular log level for each etcd package (eg: 'etcdmain=CRITICAL,etcdserver=DEBUG')
  "log-output": "default" # Specify 'stdout' or 'stderr' to skip journald logging even when running under systemd

# Certificate authority and certificate files for etcd
  - ca-etcd.pem        # client server TLS trusted CA key file/peer server TLS trusted CA file
  - ca-etcd-key.pem    # CA key file
  - cert-etcd.pem      # peer server TLS cert file
  - cert-etcd-key.pem  # peer server TLS key file

Make sure to change etcd_interface to wg0 instead of tap0 if you followed my blog series so far and used WireGuard VPN and the interface wg0. That’s important as the etcd cluster nodes and the Kubernetes API server of course must be able to talk to each other!

The etcd default flags/settings defined in etcd_settings can be overriden by defining a variable called etcd_settings_user. You can also add additional settings by using this variable. E.g. to override the default value for log-output setting add a new setting like grpc-keepalive-min-time add the following settings to group_vars/all.yml:

  "log-output": "stdout"
  "grpc-keepalive-min-time": "10s"

The role will search for the certificates we created in certificate authority in the directory you specify in etcd_ca_conf_directory (which in our case is just the value of k8s_ca_conf_directory) on the host you run Ansible. The files used here are listed in etcd_certificates.

We can deploy the role now via

ansible-playbook --tags=role-etcd k8s.yml

This will install the etcd cluster and start the etcd daemons. Have a look at the logs of your etcd hosts if everything worked and the etcd nodes are connected. Use journalct --no-pager or journalctl -f or journalctl -t etcd to check the systemd log.

Log into one of the etcd nodes and check the cluster status e.g. via

# The value of this variable should be the value of Ansible variable "etcd_ca_conf_directory"
export CERTIFICATE_DIR="/path/where/your/certificates/are/located"

ETCDCTL_API=3 etcdctl endpoint health \
  --endpoints=https://etcd-node1:2379,https://etcd-node2:2379,https://etcd-node3:2379 \
  --cacert=${CERTIFICATE_DIR}/ca-etcd.pem \
  --cert=${CERTIFICATE_DIR}/cert-etcd.pem \

Of course replace etcd-node(1-3) with your etcd node names or IPs. You should see now a output similar to this:

https://etcd-node1:2379 is healthy: successfully committed proposal: took = 9.416983ms
https://etcd-node2:2379 is healthy: successfully committed proposal: took = 6.206849ms
https://etcd-node3:2379 is healthy: successfully committed proposal: took = 8.409447ms

Next we’ll install the Kubernetes control plane.