Si estas intentando montar un cluster de Kubernetes en alta disponibilidad, o bien tienes alguna experiencia en esto porque lo has visto anteriormente o porque ya has trabajado con ello antes, o bien has llegado a este artículo buscando ayuda en Internet después de haber pasado un buen rato intentando seguir sin éxito los pasos de la incorrecta documentación oficial de multi-master en Kubernetes que hay publicada al respecto.

Bien por este motivo o bien porque quieres tener un servicio de etcd tolerante a fallos para tu proyecto, espero que con este artículo no te quede ninguna duda sobre cómo montar un cluster para conseguir alta disponibilidad y comunicaciones totalmente cifradas con SSL en el servicio de etcd.

Así que basta ya de hablar y vamos al turrón...

Para intentar organizarlo un poco, voy a dividir el artículo en varias partes:

  1. ¿Qué es etcd?
  2. Consideraciones iniciales de nuestro cluster
  3. Creación de los certificados SSL
  4. Instalación y configuración del cluster
  5. Administración básica
  6. Referencias

¿Qué es etcd?

Etcd es un servidor de almacenamiento de datos de tipo clave-valor (similar a Redis, aunque con bastantes diferencias) diseñado para funcionar de forma distribuida, rápida y estable.

Forma parte del sistema CoreOS, y es un proyecto de código abierto escrito en Go y liberado bajo licencia Apache 2.0.

Etcd es un proyecto maduro y que lleva muchos años en desarrollo constante por lo que muchos grandes proyectos lo utilizan para almacenar sus configuraciones debido a su gran estabilidad y rapidez, pero últimamente su desarrollo se ha visto potenciado aún más sobre todo por el auge que está teniendo Kubernetes, que es uno de estos grandes proyectos que utilizan etcd.

Toda la comunicación con etcd se realiza a través de una API que puede ser explotada mediante JSON a través de HTTP/HTTPS o a través del cliente etcdctl para linea de comandos.

A principios de año (enero de 2018), CoreOS (y por lo tanto etcd) fué comprado por Redhat.

Consideraciones iniciales de nuestro cluster

Como recomendación general, para montar un cluster en alta disponibilidad debemos utilizar el mayor número posible de nodos.

En cualquier caso, debemos utilizar al menos 3 nodos para el cluster ya que aunque teóricamente bastaría con 2 nodos para tener tolerancia a fallos, en un cluster de etcd siempre debe haber un nodo principal (llamado leader) que es el que tendrá el control de determinadas funciones del cluster, y si ese nodo leader quedara fuera de servicio por algún motivo, los demás nodos del cluster utilizarían un quórum para designar quién sería el nuevo leader. El algoritmo que se utiliza para obtener dicho quórum es Raft, usado no solo para este proyecto sino para muchísimos otros, por lo tanto debe haber al menos 3 nodos para que si se pierde el nodo leader los otros 2 puedan designar un sustituto.

Así que en este artículo vamos a crear un cluster con 3 nodos que tendrán estas características:

# Nombre IP RAM
1 etcd01 192.168.1.83 2 Gb
2 etcd02 192.168.1.84 2 Gb
3 etcd03 192.168.1.85 2 Gb

Todas estas máquinas parten de una instalación limpia de mi sistema operativo favorito (Debian, concretamente la versión stable 9.4) pero debería funcionar prácticamente igual (con ligeros cambios en algunos paths o en el nombre de algún paquete) en cualquier otra distribución de Linux.

Antes de empezar, asegúrate de que todos estos servidores tienen conectividad entre ellos, y que resuelven los nombres de los demás. Lo mejor sería que tuvieras tu propio servidor DNS (bind o similar) configurado en tu red para resolver todos los nombres de tus servidores, pero sino, puedes añadir las 3 direcciones IP con sus respectivos nombres al archivo /etc/hosts de cada uno de ellos:

/etc/hosts download
127.0.0.1       localhost
192.168.1.83    etcd01
192.168.1.84    etcd02
192.168.1.85    etcd03

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

Ni qué decir tiene que es recomendable asegurarse siempre de que partimos de un sistema actualizado:

$ sudo apt-get update
$ sudo apt-get dist-upgrade

En mi caso, las últimas versiones disponibles en el momento de escribir este artículo son (no te preocupes si alguno no te suena, los iremos viendo a medida que los vayamos necesitando):

  • Etcd: 3.3.4
  • cfssl: 1.2
  • cfssljson: 1.2

Creación de los certificados SSL

Como acabo de comentar, toda la comunicación tanto entre el cliente y el servidor de etcd como entre los propios servidores de etcd de un cluster puede realizarse por SSL, cifrando de esa forma todas las comunicaciones y aportando otra capa de seguridad al sistema.

Aunque podríamos montar el cluster utilizando únicamente HTTP, es muy sencillo generar unos certificados y configurar etcd para que los use, por lo que vamos a hacerlo de esta forma.

Lo primero que debemos hacer será generar los certificados que vamos a necesitar, que son:

  • Cliente: Este certificado es el que usaremos nosotros (o la aplicación que se vaya a conectar a etcd, como por ejemplo Kubernetes) para conectarnos al cluster.
  • Servidor: Este certificado es el que usará cada uno de los nodos de nuestro cluster para cifrar las comunicaciones.
  • Peer (no se muy bien como traducirlo): Este certificado es el que usará cada nodo para conectarse a los demás nodos del cluster (muy similar al certificado de cliente, pero generaremos ambos para diferenciar unas conexiones de otras).

La explicación sobre el intercambio de certificados en una comunicación SSL, los tipos de certificados, su firma, etc. es algo que queda fuera del ámbito de este artículo por lo que no voy a explicarlo con demasiado detalle. Si alguno de estos conceptos no te queda claro, te recomiendo que investigues un poco acerca del funcionamiento de estos certificados (intentaré escribir otro artículo sobre SSL algún dia).

Para generar los certificados vamos a utilizar cfssl, que es una herramienta de código-abierto desarrollada por CloudFlare muy fácil de utilizar y que nos permite generar y firmar certificados desde la linea de comandos.

En lugar de ir nodo por nodo, vamos a instalar la herramienta en nuestra máquina local para generar los certificados, y una vez generados los copiaremos a los 3 nodos.

Como dije antes, en el momento de escribir este artículo la última versión de cfssl era la 1.2, por lo que os recomiendo que antes de instalarla comprobéis en su página (https://pkg.cfssl.org/) que no haya ninguna versión más reciente, y en caso de haberla, cambiad las siguientes URLs según corresponda para poder instalar la aplicación actualizada:

$ sudo curl -o /usr/local/bin/cfssl https://pkg.cfssl.org/R1.2/cfssl_linux-amd64
$ sudo curl -o /usr/local/bin/cfssljson https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64
$ sudo chmod +x /usr/local/bin/cfssl*

Creamos un directorio en cualquier parte de nuestro disco duro (y nos situamos en él) para ir guardando los certificados de forma temporal hasta que los hayamos copiado a los nodos de nuestro cluster:

$ mkdir $HOME/etcd-certs
$ cd $HOME/etcd-certs

Y ahora vamos creando los certificados necesarios:

Certificado de la Autoridad de Certificación (CA)

Primero, crearemos un archivo JSON llamado ca-config.json con la configuración de la Autoridad de Certificación (CA) que va a firmar los certificados que vamos a generar.

En él definiremos la expiración de los certificados, el uso que les vamos a dar (autenticación, firma, ...), etc:

ca-config.json download
{
  "signing": {
    "default": {
      "expiry":"43800h"
    },
    "profiles": {
      "server": {
        "expiry":"43800h",
        "usages": [
          "signing",
          "key encipherment",
          "server auth",
          "client auth"
        ]
      },
      "client": {
        "expiry":"43800h",
        "usages": [
          "signing",
          "key encipherment",
          "client auth"
        ]
      },
      "peer": {
        "expiry":"43800h",
        "usages": [
          "signing",
          "key encipherment",
          "server auth",
          "client auth"
        ]
      }
    }
  }
}

Después crearemos otro archivo llamado ca-csr.json donde irá la configuración de la solicitud de certificación (CSR), donde indicaremos el algoritmo de cifrado que queremos utilizar, la longitud de la clave, los datos del certificado, etc:

ca-csr.json download
{
  "CN":"etcd",
  "key": {
    "algo":"rsa",
    "size":2048
  },
  "names": [
    {
      "C":"ES",
      "L":"Madrid",
      "O":"PornoHardware S.L.U.",
      "OU":"Tech department",
      "ST":"Spain"
    }
  ]
}

Generamos los certificados de nuestra Autoridad de Certificación:

$ cfssl gencert -initca ca-csr.json | cfssljson -bare ca -

Una vez ejecutado el comando habremos obtenido 3 nuevos archivos:

  • ca.csr: La petición de generación de certificado.
  • ca.pem: El certificado raíz.
  • ca-key.pem: La clave del certificado raíz.

Ya tenemos nuestro certificado raíz, vamos a seguir con los demas...

Certificado de cliente

Este certificado es el que usaremos nosotros mismos para conectarnos al cluster y lanzar los comandos que queramos ejecutar de forma segura.

Creamos el archivo client.json:

client.json download
{
  "CN": "client",
  "key": {
    "algo": "ecdsa",
    "size": 256
  }
}

Y utilizando el recién creado certificado raíz y la configuración que habíamos definido al principio, generamos el certificado de cliente:

$ cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client client.json | cfssljson -bare client

Una vez ejecutado el comando habremos obtenido 3 nuevos archivos:

  • client.csr: La petición de generación de certificado.
  • client.pem: El certificado de cliente.
  • client-key.pem: La clave del certificado de cliente.

Certificados peer y de servidor

Cada uno de los nodos necesita también un certificado para cifrar las conexiones que los demás nodos se hagan entre si (peer). Este certificado necesita generarse para un nombre de host y una IP en concreto (la de cada servidor), por lo tanto necesitamos generar 3 certificados diferentes (uno para cada nodo de nuestro cluster).

Creamos un archivo de configuración por cada nodo, donde indicaremos el nombre del servidor y su IP, entre otras cosas:

Para el nodo 1:

config-etcd01.json download
{
  "CN": "etcd01",
  "hosts": [
    "etcd01",
    "etcd01.local",
    "192.168.1.83"
  ],
  "key": {
    "algo": "ecdsa",
    "size": 256
  },
  "names": [
    {
      "C": "ES",
      "L": "Madrid",
      "ST": "Spain"
    }
  ]
}

Para el nodo 2:

config-etcd02.json download
{
  "CN": "etcd02",
  "hosts": [
    "etcd02",
    "etcd02.local",
    "192.168.1.84"
  ],
  "key": {
    "algo": "ecdsa",
    "size": 256
  },
  "names": [
    {
      "C": "ES",
      "L": "Madrid",
      "ST": "Spain"
    }
  ]
}

Para el nodo 3:

config-etcd03.json download
{
  "CN": "etcd03",
  "hosts": [
    "etcd03",
    "etcd03.local",
    "192.168.1.85"
  ],
  "key": {
    "algo": "ecdsa",
    "size": 256
  },
  "names": [
    {
      "C": "ES",
      "L": "Madrid",
      "ST": "Spain"
    }
  ]
}

Como todos los certificados que vamos a generar tendrán el mismo nombre (server.pem, server-key.pem, ...) independientemente del nodo al que correspondan, vamos a crear un subdirectorio por cada nodo y a generar los certificados de cada uno de ellos en su respectivo directorio.

Creamos el subdirectorio, entramos en él, generamos los certificados (tanto los de servidor como los peer) y continuamos con el siguiente sucesivamente hasta que hayamos generado los certificados de los 3 servidores:

$ mkdir $HOME/etcd-certs/etcd01
$ cd $HOME/etcd-certs/etcd01
$ cfssl gencert -ca=../ca.pem -ca-key=../ca-key.pem -config=../ca-config.json -profile=server ../config-etcd01.json | cfssljson -bare server
$ cfssl gencert -ca=../ca.pem -ca-key=../ca-key.pem -config=../ca-config.json -profile=peer ../config-etcd01.json | cfssljson -bare peer
$ mkdir $HOME/etcd-certs/etcd02
$ cd $HOME/etcd-certs/etcd02
$ cfssl gencert -ca=../ca.pem -ca-key=../ca-key.pem -config=../ca-config.json -profile=server ../config-etcd02.json | cfssljson -bare server
$ cfssl gencert -ca=../ca.pem -ca-key=../ca-key.pem -config=../ca-config.json -profile=peer ../config-etcd02.json | cfssljson -bare peer
$ mkdir $HOME/etcd-certs/etcd03
$ cd $HOME/etcd-certs/etcd03
$ cfssl gencert -ca=../ca.pem -ca-key=../ca-key.pem -config=../ca-config.json -profile=server ../config-etcd03.json | cfssljson -bare server
$ cfssl gencert -ca=../ca.pem -ca-key=../ca-key.pem -config=../ca-config.json -profile=peer ../config-etcd03.json | cfssljson -bare peer

Una vez ejecutados todos los comandos habremos obtenido 6 nuevos archivos en cada subdirectorio:

  • peer.csr: La petición de generación de certificado.
  • peer.pem: El certificado peer.
  • peer-key.pem: La clave del certificado peer.
  • server.csr: La petición de generación de certificado.
  • server.pem: El certificado de servidor.
  • server-key.pem: La clave del certificado de servidor.

Si todo ha ido bien, ahora deberíamos tener todos los certificados necesarios para nuestro cluster generados en $HOME/etcd-certs/, por lo que deberíamos tener algo parecido a ésto:

├─ $HOME/etcd-certs/
│  ├─ ca.csr
│  ├─ ca.pem
│  ├─ ca-config.json
│  ├─ ca-csr.json
│  ├─ ca-key.pem
│  ├─ client.csr
│  ├─ client.json
│  ├─ client.pem
│  ├─ client-key.pem
│  ├─ config-etcd01.json
│  ├─ config-etcd02.json
│  ├─ config-etcd02.json
│  ├─ etcd01/
│  │  ├─ peer.csr
│  │  ├─ peer.pem
│  │  ├─ peer-key.pem
│  │  ├─ server.csr
│  │  ├─ server.pem
│  │  └─ server-key.pem
│  ├─ etcd02/
│  │  ├─ peer.csr
│  │  ├─ peer.pem
│  │  ├─ peer-key.pem
│  │  ├─ server.csr
│  │  ├─ server.pem
│  │  └─ server-key.pem
│  └─ etcd03/
│     ├─ peer.csr
│     ├─ peer.pem
│     ├─ peer-key.pem
│     ├─ server.csr
│     ├─ server.pem
│     └─ server-key.pem

Ya tenemos todos los archivos que necesitamos para poder configurar nuestro cluster de etcd utilizando SSL, por lo que ya solo nos queda copiarlos a cada uno de los nodos.

No hace falta que copiemos todos los archivos a cada nodo, únicamente vamos a necesitar estos:

  • ca.pem
  • client.pem
  • client-key.pem

Y para cada uno de los nodos únicamente los de su respectivo subdirectorio:

  • etcdXX/server.pem
  • etcdXX/server-key.pem
  • etcdXX/peer.pem
  • etcdXX/peer-key.pem

Como ya comenté antes, en mi caso estoy montando este cluster de etcd para su uso con Kubernetes por lo que voy a copiar los certificados al directorio /etc/kubernetes/pki/etcd de cada nodo, pero vosotros podéis copiarlos al directorio que queráis (/etc/etcd/certs, por ejemplo).

Una vez copiados estos 7 archivos a cada nodo ya podemos empezar con la configuración del cluster.

Importante: Aunque no todos los archivos generados hacen falta os recomiendo que los guardéis igualmente (sobre todo los archivos de configuración) por si en un futuro necesitáis volver a generar los certificados de alguno de los servidores o generar nuevos certificados para un nuevo nodo con el que ampliar el cluster.

Instalación y configuración del cluster

Antes de nada, si los nodos que vas a utilizar para el cluster tienen algún firewall configurado, debemos abrir los puertos necesarios para poder conectarnos.

Etcd utiliza estos puertos por defecto:

  • 2379: Para las conexiones de los clientes
  • 2380: Para las conexiones entre los demás nodos del cluster

Por lo tanto debemos asegurarnoss de que podemos conectarnos al puerto 2380 de cualquier nodo desde cualquier nodo y al puerto 2379 de cualquier nodo desde donde vayamos a hacer peticiones al cluster.

Es importante comprobar que no tengamos en nuestros nodos ninguna versión de etcd ya instalada. Si es así, hay que desinstalarla primero.

Antes de instalar una nueva versión de etcd tenemos que saber cuál es esa última versión, por lo que vamos a la zona de releases de su página de Github (https://github.com/coreos/etcd/releases) y lo comprobamos.

En este momento la última versión es la 3.3.4, por lo que descargamos e instalamos dicha versión en cada uno de los nodos de nuestro cluster:

$ export VERSION=v3.3.4
$ curl -sSL https://github.com/coreos/etcd/releases/download/$VERSION/etcd-$VERSION-linux-amd64.tar.gz | sudo tar -xzv --strip-components=1 -C /usr/local/bin/

Ahora que ya tenemos etcd instalado en los 3 nodos, vamos a configurarlo.

Creamos un archivo /etc/etdc.env en cada uno de los nodos con el nombre e IP de cada servidor:

Para el nodo 1:

/etc/etcd.env download
PEER_NAME=etcd01
PRIVATE_IP=192.168.1.83

Para el nodo 2:

/etc/etcd.env download
PEER_NAME=etcd02
PRIVATE_IP=192.168.1.84

Para el nodo 3:

/etc/etcd.env download
PEER_NAME=etcd03
PRIVATE_IP=192.168.1.85

Ahora vamos a crear los archivos de configuración de systemd para que podamos arrancar, reiniciar y parar el servicio de etcd como haríamos con cualquier otro servicio.

Para eso, creamos un archivo /etc/systemd/system/etcd.service en cada uno de los nodos, indicando los argumentos que se le pasarán al programa etcd cuando arranque, y que son básicamente el nombre e IP del servidor, el nombre e IP de los demás nodos del cluster, la ruta a los certificados que generamos en el paso anterior, etc.

El archivo quedaría más o menos así para el nodo 1:

/etc/systemd/system/etcd.service download
[Unit]
Description=etcd
Documentation=https://github.com/coreos/etcd
Conflicts=etcd.service
Conflicts=etcd2.service

[Service]
EnvironmentFile=/etc/etcd.env
Type=notify
Restart=always
RestartSec=5s
LimitNOFILE=40000
TimeoutStartSec=0

ExecStart=/usr/local/bin/etcd \
  --name etcd01 \
  --data-dir /var/lib/etcd \
  --initial-advertise-peer-urls https://192.168.1.83:2380 \
  --listen-peer-urls https://192.168.1.83:2380 \
  --listen-client-urls https://192.168.1.83:2379,https://127.0.0.1:2379 \
  --advertise-client-urls https://192.168.1.83:2379 \
  --initial-cluster-token etcd-cluster-1 \
  --initial-cluster etcd01=https://192.168.1.83:2380,etcd02=https://192.168.1.84:2380,etcd03=https://192.168.1.85:2380 \
  --initial-cluster-state new \
  --cert-file=/etc/kubernetes/pki/etcd/server.pem \
  --key-file=/etc/kubernetes/pki/etcd/server-key.pem \
  --client-cert-auth \
  --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.pem \
  --peer-cert-file=/etc/kubernetes/pki/etcd/peer.pem \
  --peer-key-file=/etc/kubernetes/pki/etcd/peer-key.pem \
  --peer-client-cert-auth \
  --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.pem

[Install]
WantedBy=multi-user.target

Para el nodo 2:

/etc/systemd/system/etcd.service download
[Unit]
Description=etcd
Documentation=https://github.com/coreos/etcd
Conflicts=etcd.service
Conflicts=etcd2.service

[Service]
EnvironmentFile=/etc/etcd.env
Type=notify
Restart=always
RestartSec=5s
LimitNOFILE=40000
TimeoutStartSec=0

ExecStart=/usr/local/bin/etcd \
  --name etcd02 \
  --data-dir /var/lib/etcd \
  --initial-advertise-peer-urls https://192.168.1.84:2380 \
  --listen-peer-urls https://192.168.1.84:2380 \
  --listen-client-urls https://192.168.1.84:2379,https://127.0.0.1:2379 \
  --advertise-client-urls https://192.168.1.84:2379 \
  --initial-cluster-token etcd-cluster-1 \
  --initial-cluster etcd01=https://192.168.1.83:2380,etcd02=https://192.168.1.84:2380,etcd03=https://192.168.1.85:2380 \
  --initial-cluster-state new \
  --cert-file=/etc/kubernetes/pki/etcd/server.pem \
  --key-file=/etc/kubernetes/pki/etcd/server-key.pem \
  --client-cert-auth \
  --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.pem \
  --peer-cert-file=/etc/kubernetes/pki/etcd/peer.pem \
  --peer-key-file=/etc/kubernetes/pki/etcd/peer-key.pem \
  --peer-client-cert-auth \
  --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.pem

[Install]
WantedBy=multi-user.target

Para el nodo 3:

/etc/systemd/system/etcd.service download
[Unit]
Description=etcd
Documentation=https://github.com/coreos/etcd
Conflicts=etcd.service
Conflicts=etcd2.service

[Service]
EnvironmentFile=/etc/etcd.env
Type=notify
Restart=always
RestartSec=5s
LimitNOFILE=40000
TimeoutStartSec=0

ExecStart=/usr/local/bin/etcd \
  --name etcd03 \
  --data-dir /var/lib/etcd \
  --initial-advertise-peer-urls https://192.168.1.85:2380 \
  --listen-peer-urls https://192.168.1.85:2380 \
  --listen-client-urls https://192.168.1.85:2379,https://127.0.0.1:2379 \
  --advertise-client-urls https://192.168.1.85:2379 \
  --initial-cluster-token etcd-cluster-1 \
  --initial-cluster etcd01=https://192.168.1.83:2380,etcd02=https://192.168.1.84:2380,etcd03=https://192.168.1.85:2380 \
  --initial-cluster-state new \
  --cert-file=/etc/kubernetes/pki/etcd/server.pem \
  --key-file=/etc/kubernetes/pki/etcd/server-key.pem \
  --client-cert-auth \
  --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.pem \
  --peer-cert-file=/etc/kubernetes/pki/etcd/peer.pem \
  --peer-key-file=/etc/kubernetes/pki/etcd/peer-key.pem \
  --peer-client-cert-auth \
  --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.pem

[Install]
WantedBy=multi-user.target

Ya solo nos queda reiniciar el demonio de systemd de cada nodo para que lea el archivo que acabamos de crear:

$ sudo systemctl daemon-reload

Y ahora ya podemos gestionar el demonio de etcd con el comando systemctl o con service, por lo que vamos a levantar el proceso y a configurarlo para que se inicie automáticamente al arrancar el servidor:

$ sudo systemctl enable etcd
$ sudo systemctl start etcd

Si todo ha ido bien, ahora mismo ya tendremos el servicio de etcd levantado, por lo que hacemos lo mismo en los otros 2 nodos.

Si es la primera vez que estas levantando el servicio, hay veces en las que al invocar a systemctl se queda el proceso esperando eternamente (hasta que salta el más-que-generoso-timeout(TM), me refiero). La mayoría de las veces se debe a que el proceso se demonio de etcd se inicia correctamente, pero se queda intentando conectarse con los otros 2 nodos del cluster, por lo que si aún no los hemos levantado, el proceso lo reintenta indefinidamente y parece que se queda congelado...

En estos casos, puesto que el proceso se levanta correctamente, o bien sales con CTRL + C, o bien levantas los demonios en los otros 2 nodos.

Puedes comprobar si el servicio se ha levantado correctamente con:

$ sudo systemctl status etcd
- etcd.service - etcd
   Loaded: loaded (/etc/systemd/system/etcd.service; enabled; vendor preset: enabled)
   Active: active (running) since Tue 2018-05-06 00:21:53 CEST; 11s ago
     Docs: https://github.com/coreos/etcd
 Main PID: 9426 (etcd)
    Tasks: 18 (limit: 4915)
   Memory: 10.1M
      CPU: 145ms
   CGroup: /system.slice/etcd.service
           └─9426 /usr/local/bin/etcd --name etcd01 --data-dir /var/lib/etcd --initial-advertise-peer-urls https://192.168.1.83:2380 --listen-peer-urls https://192.168.1.83:2380 --listen-client-urls https://192.168.1.83:2379,https://127.0.0.1:2379 --advertise-client-urls

May 06 00:21:53 etcd01 etcd[20483]: established a TCP streaming connection with peer c37956d0928a73d9 (stream Message reader)
May 06 00:21:53 etcd01 etcd[20483]: established a TCP streaming connection with peer c37956d0928a73d9 (stream MsgApp v2 reader)
May 06 00:21:53 etcd01 etcd[20483]: raft.node: 1e9f81657a2688f9 elected leader b5a3b7906f500074 at term 1291
May 06 00:21:53 etcd01 etcd[20483]: 1e9f81657a2688f9 initialzed peer connection; fast-forwarding 8 ticks (election ticks 10) with 2 active peer(s)
May 06 00:21:53 etcd01 etcd[20483]: published {Name:etcd01 ClientURLs:["https://192.168.1.83:2379"]} to cluster e47a206dc7abb150
May 06 00:21:53 etcd01 etcd[20483]: ready to serve client requests
May 06 00:21:53 etcd01 etcd[20483]: ready to serve client requests
May 06 00:21:53 etcd01 systemd[1]: Started etcd.
May 06 00:21:53 etcd01 etcd[20483]: serving client requests on 127.0.0.1:2379
May 06 00:21:53 etcd01 etcd[20483]: serving client requests on 192.168.1.83:2379

Esto significa que ya tenemos nuestro cluster funcionando y aceptando conexiones por el puerto 2379!

Administración básica

Ahora que ya tenemos el cluster funcionando, vamos a ver algunos comandos que nos serán de utilidad a la hora de administrar el cluster, comprobar su estado, añadir un nuevo nodo, etc.

Recuerda que hemos configurado nuestro cluster para usar SSL en cualquier conexión, por lo que necesitaremos el certificado de cliente en cualquier sitio desde el que nos queramos conectar al cluster.

Al servidor etcd se accede utilizando la API, como ya expliqué al principio del artículo, pero existe una utilidad llamada etcdctl que nos permite acceder a dicha API desde la linea de comandos. Vamos a ver sus opciones, pero antes tenemos que establecer la variable de sesión ETCDCTL_API=3 ya que de lo contrario la utilidad nos mostrará opciones obsoletas (luego explicaré el tema de las variables de sesión):

$ export ETCDCTL_API=3
$ etcdctl --help
NAME:
    etcdctl - A simple command line client for etcd3.

USAGE:
    etcdctl

VERSION:
    3.3.4

API VERSION:
    3.3


COMMANDS:
    get         Gets the key or a range of keys
    put         Puts the given key into the store
    del         Removes the specified key or range of keys [key, range_end)
    txn         Txn processes all the requests in one transaction
    compaction      Compacts the event history in etcd
    alarm disarm        Disarms all alarms
    alarm list      Lists all alarms
    defrag          Defragments the storage of the etcd members with given endpoints
    endpoint health     Checks the healthiness of endpoints specified in `--endpoints` flag
    endpoint status     Prints out the status of endpoints specified in `--endpoints` flag
    endpoint hashkv     Prints the KV history hash for each endpoint in --endpoints
    move-leader     Transfers leadership to another etcd cluster member.
    watch           Watches events stream on keys or prefixes
    version         Prints the version of etcdctl
    lease grant     Creates leases
    lease revoke        Revokes leases
    lease timetolive    Get lease information
    lease list      List all active leases
    lease keep-alive    Keeps leases alive (renew)
    member add      Adds a member into the cluster
    member remove       Removes a member from the cluster
    member update       Updates a member in the cluster
    member list     Lists all members in the cluster
    snapshot save       Stores an etcd node backend snapshot to a given file
    snapshot restore    Restores an etcd member snapshot to an etcd directory
    snapshot status     Gets backend snapshot status of a given file
    make-mirror     Makes a mirror at the destination etcd cluster
    migrate         Migrates keys in a v2 store to a mvcc store
    lock            Acquires a named lock
    elect           Observes and participates in leader election
    auth enable     Enables authentication
    auth disable        Disables authentication
    user add        Adds a new user
    user delete     Deletes a user
    user get        Gets detailed information of a user
    user list       Lists all users
    user passwd     Changes password of user
    user grant-role     Grants a role to a user
    user revoke-role    Revokes a role from a user
    role add        Adds a new role
    role delete     Deletes a role
    role get        Gets detailed information of a role
    role list       Lists all roles
    role grant-permission   Grants a key to a role
    role revoke-permission  Revokes a key from a role
    check perf      Check the performance of the etcd cluster
    help            Help about any command

OPTIONS:
      --cacert=""               verify certificates of TLS-enabled secure servers using this CA bundle
      --cert=""                 identify secure client using this TLS certificate file
      --command-timeout=5s          timeout for short running command (excluding dial timeout)
      --debug[=false]               enable client-side debug logging
      --dial-timeout=2s             dial timeout for client connections
  -d, --discovery-srv=""            domain name to query for SRV records describing cluster endpoints
      --endpoints=[127.0.0.1:2379]      gRPC endpoints
      --hex[=false]             print byte strings as hex encoded strings
      --insecure-discovery[=true]       accept insecure SRV records describing cluster endpoints
      --insecure-skip-tls-verify[=false]    skip server certificate verification
      --insecure-transport[=true]       disable transport security for client connections
      --keepalive-time=2s           keepalive time for client connections
      --keepalive-timeout=6s            keepalive timeout for client connections
      --key=""                  identify secure client using this TLS key file
      --user=""                 username[:password] for authentication (prompt if password is not supplied)
  -w, --write-out="simple"          set the output format (fields, json, protobuf, simple, table)

Como puedes ver este comando tiene bastantes parámetros, por lo que vamos a echar un ojo a los más importantes:

  • --endpoints: Aqui debemos especificar todos los nodos de nuestro cluster (host y puerto) separados por comas, de forma que si uno de ellos estuviera caído nuestra consulta sería enviada de forma automática a otro de los nodos. Por ejemplo, en nuestro caso sería: https://etcd01:2379,https://etcd02:2379,https://etcd03:2379.
  • --cert: La ruta del certificado de cliente que vamos a utilizar para conectarnos. En nuestro caso sería: /etc/kubernetes/pki/etcd/client.pem.
  • --key: La ruta de la clave del certificado de cliente. En nuestro caso sería: /etc/kubernetes/pki/etcd/client-key.pem.
  • --cacert: La ruta hacia la entidad certificadora con la que hemos generado los certificados. En nuestro caso sería: /etc/kubernetes/pki/etcd/ca.pem.
  • --write-out: Aqui indicamos cómo queremos que nos devuelva al respuesta del comando que vamos a lanzar al cluster. Puede ser simple, json, table, etc.
  • --debug: Con esta opción podemos ver las llamadas HTTP/HTTPS que se estan lanzando a la API del cluster para ejecutar el comando que vayamos a ejecutar. Muy util para depurar el proceso si tenemos algún problema con los datos que estamos obteniendo.

Por comodidad, tener que estar introduciendo las rutas de todos los certificados y la lista de endpoints cada vez que lancemos un comando puede resultar una auténtica tortura. Por suerte, podemos definir una serie de variables de entorno que una vez definidas permitirán a etcdctl conocer el valor de todos estos parámetros sin que tengamos que estar tecleándolos constantemente. Estas variables son:

  • ETCDCTL_CERT: La ruta que pondríamos en el parámetro --cert-file.
  • ETCDCTL_KEY: La ruta que pondríamos en el parámetro --key-file.
  • ETCDCTL_CACERT: La ruta que pondríamos en el parámetro --ca-file.
  • ETCDCTL_ENDPOINTS: La lista (separada por comas) de endpoints (host y puerto) de nuestro cluster.
  • ETCDCTL_API: Versión de la API que queramos utilizar. Puede ser 2 o 3.

Una vez que hemos visto los parámetros, vamos con algunos de los comandos disponibles:

  • get: Devuelve el valor de una clave.
  • put: Establece el valor de una clave.
  • endpoint health: Muestra el estado de los nodos del cluster.
  • check perf: Realiza un informe de rendimiento del cluster.
  • Etc... (esta vez me refiero a etcétera, no al nombre del programa del que estamos hablando xDDD)

Vamos a probar alguno de ellos...

Nos conectamos a cualquiera de los nodos (por ejemplo al primero: etcd01) y definimos las variables de sesión con los valores necesarios para no tener que preocuparnos de ellos en cada ejecución del comando etcdctl:

$ export ETCDCTL_CERT=/etc/kubernetes/pki/etcd/client.pem
$ export ETCDCTL_KEY=/etc/kubernetes/pki/etcd/client-key.pem
$ export ETCDCTL_CACERT=/etc/kubernetes/pki/etcd/ca.pem
$ export ETCDCTL_ENDPOINTS=https://etcd01:2379,https://etcd02:2379,https://etcd03:2379
$ export ETCDCTL_API=3

Ahora vamos a lanzar el comando endpoint health para ver la salud de nuestro recién creado cluster:

$ etcdctl endpoint health
https://etcd02:2379 is healthy: successfully committed proposal: took = 2.397419ms
https://etcd01:2379 is healthy: successfully committed proposal: took = 1.535434ms
https://etcd03:2379 is healthy: successfully committed proposal: took = 2.331939ms

Que indica que los 3 nodos estan funcionando correctamente (...is healthy).

Si en lugar del resultado os muestra algún error relacionado con los certificados significa que no habeis puesto bien las rutas de las variables de sesión, o que vuestro usuario no tiene permiso para leer dichos archivos.

Ahora vamos a introducir una clave de prueba y su valor. Por ejemplo:

$ etcdctl put clavePrueba "Esto es una prueba"

Leemos el valor de la clave clavePrueba:

$ etcdctl get clavePrueba
Esto es una prueba

Vamos ahora con algo más delicado: gestionar nuevos nodos. Supongamos que queremos añadir un nuevo nodo a nuestro cluster. Por comodidad y para matar dos pájaros de un tiro vamos primero a quitar uno de los nodos del cluster y luego lo volveremos a meter.

Con el comando member list vemos la lista de nodos que componen nuestro cluster, y el ID de cada uno de ellos:

$ etcdctl member list
b5a3b7906f500074, started, etcd01, https://192.168.1.83:2380, https://192.168.1.83:2379
c37956d0928a73d9, started, etcd02, https://192.168.1.84:2380, https://192.168.1.84:2379
1e9f81657a2688f9, started, etcd03, https://192.168.1.85:2380, https://192.168.1.85:2379

Para eliminar uno de ellos, por ejemplo el nodo 3 (etcd03), ejecutamos el comando member remove indicando el ID del nodo que queremos eliminar de nuestro cluster:

$ etcdctl member remove 1e9f81657a2688f9
Member 1e9f81657a2688f9 removed from cluster e47a206dc7abb150

Si volvemos a obtener la lista de nodos del cluster veremos que ya solo tenemos 2:

$ etcdctl member list
b5a3b7906f500074, started, etcd01, https://192.168.1.83:2380, https://192.168.1.83:2379
c37956d0928a73d9, started, etcd02, https://192.168.1.84:2380, https://192.168.1.84:2379

El procedimiento para añadir un nodo es tambien muy sencillo, pero si el nodo que queremos añadir ya ha formado parte de un cluster etcd anteriormente (como es nuestro caso ya que lo acabamos de eliminar ahora mismo) hay que hacer un par de cosas antes:

  • Detener el servicio de etcd en el nuevo nodo que queremos añadir: $ sudo systemctl stop etcd.
  • Eliminar el directorio donde se guardan los datos de etcd para que al añadirlo al cluster se sincronice de nuevo con el resto de nodos: $ sudo rm -rf /var/lib/etcd/member.

Una vez hecho esto, ya podemos continuar con el proceso para añadirlo a nuestro cluster.

Ejecutamos el comando member add junto con el nombre y el endpoint de peer del nuevo nodo que queremos añadir:

$ etcdctl member add etcd03 --peer-urls="https://192.168.1.85:2380"
Member c398e05912695024 added to cluster e47a206dc7abb150
ETCD_NAME="etcd03"
ETCD_INITIAL_CLUSTER="etcd01=https://192.168.1.83:2380,etcd02=https://192.168.1.84:2380,etcd03=https://192.168.1.85:2380"
ETCD_INITIAL_ADVERTISE_PEER_URLS="https://192.168.1.85:2380"
ETCD_INITIAL_CLUSTER_STATE="existing"

El comando muestra los datos del nuevo nodo que hemos añadido (nombre, endpoints, etc) así como su estado actual, que es existing. Esto significa que aunque el nodo se ha añadido al cluster, aún no está operativo. Podemos verlo si de nuevo volvemos a pedir el listado de nodos:

$ etcdctl member list
b5a3b7906f500074, started, etcd01, https://192.168.1.83:2380, https://192.168.1.83:2379
c37956d0928a73d9, started, etcd02, https://192.168.1.84:2380, https://192.168.1.84:2379
c398e05912695024, unstarted, , https://192.168.1.85:2380,

Como ves, el nuevo nodo está unstarted, por lo que aún no está funcionando (como es lógico).

Para iniciarlo, nos conectamos a dicho nodo y editamos el archivo de configuración de systemctl del etcd (el archivo /etc/systemd/system/etcd.service que creamos cuando estábamos configurando el cluster). Tenemos que sustituir la linea --initial-cluster-state new por --initial-cluster-state existing, por lo que el archivo quedaría así:

/etc/systemd/system/etcd.service download
[Unit]
Description=etcd
Documentation=https://github.com/coreos/etcd
Conflicts=etcd.service
Conflicts=etcd2.service

[Service]
EnvironmentFile=/etc/etcd.env
Type=notify
Restart=always
RestartSec=5s
LimitNOFILE=40000
TimeoutStartSec=0

ExecStart=/usr/local/bin/etcd \
  --name etcd03 \
  --data-dir /var/lib/etcd \
  --initial-advertise-peer-urls https://192.168.1.85:2380 \
  --listen-peer-urls https://192.168.1.85:2380 \
  --listen-client-urls https://192.168.1.85:2379,https://127.0.0.1:2379 \
  --advertise-client-urls https://192.168.1.85:2379 \
  --initial-cluster-token etcd-cluster-1 \
  --initial-cluster etcd01=https://192.168.1.83:2380,etcd02=https://192.168.1.84:2380,etcd03=https://192.168.1.85:2380 \
  --initial-cluster-state existing \
  --cert-file=/etc/kubernetes/pki/etcd/server.pem \
  --key-file=/etc/kubernetes/pki/etcd/server-key.pem \
  --client-cert-auth \
  --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.pem \
  --peer-cert-file=/etc/kubernetes/pki/etcd/peer.pem \
  --peer-key-file=/etc/kubernetes/pki/etcd/peer-key.pem \
  --peer-client-cert-auth \
  --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.pem

[Install]
WantedBy=multi-user.target

Recargamos la configuración de systemctl e iniciamos el servicio de etcd:

$ sudo systemctl daemon-reload
$ sudo systemctl start etcd

Al cabo de unos pocos segundos (dependiendo del tamaño de la base de datos de etcd que tengamos en nuestro cluster), el nuevo nodo se habrá sincronizado y si pedimos de nuevo la lista de nodos tendríamos que ver que el nuevo nodo ya está funcionando como los demás:

$ etcdctl member list
b5a3b7906f500074, started, etcd01, https://192.168.1.83:2380, https://192.168.1.83:2379
c37956d0928a73d9, started, etcd02, https://192.168.1.84:2380, https://192.168.1.84:2379
736e5742db963896, started, etcd03, https://192.168.1.85:2380, https://192.168.1.85:2379

Por coherencia con el resto de nodos, yo recomiendo que una vez se ha añadido el nuevo nodo al cluster y todo está funcionando, editemos de nuevo el archivo /etc/systemd/system/etcd.service y volvamos a cambiar el --initial-cluster-state existing por --initial-cluster-state new para dejarlo como estaba (igual que en el resto de nodos) pero realmente daría igual ya que una vez iniciado el servicio por primera vez, ese parámetro ya no se vuelve a tener en cuenta.

Bueno, pues ya tenemos el cluster otra vez completo, ¿pero cómo sabemos que realmente esta funcionando en alta disponibilidad, y va a seguir haciéndolo aunque alguno de sus nodos se quede fuera de servicio? Vamos a comprobarlo:

Detenemos el servicio de etcd en el nodo 1 (etcd01):

$ sudo systemctl stop etcd

Y volvemos a preguntar por el valor de la clave que guardamos antes:

$ etcdctl get clavePrueba
Esto es una prueba

Como ves, obtenemos el valor de la clave sin problemas aún con el nodo 1 fuera de servicio.

Ahora, levantamos de nuevo el servicio etcd en el nodo 1, lo detenemos el nodo 2 y volvemos a preguntar por la clave (es importante que ANTES de detener el nodo 2 vuelvas a iniciar el nodo 1, ya que un cluster de 3 nodos puede sobrevivir si falla 1 nodo, pero no si fallan 2):

$ etcdctl get clavePrueba
Esto es una prueba

De nuevo sigue funcionando... así que vamos con la última prueba: levantamos de nuevo el servicio etcd en el nodo 2, lo detenemos en el nodo 3 y volvemos a preguntar por la clave:

$ etcdctl get clavePrueba
Esto es una prueba

Como ves, el cluster sigue funcionando aunque cualquiera de sus nodos deje de hacerlo.

Ya tienes tu cluster montado y funcionado, así que ya puedes empezar a utilizarlo sin problemas!

Como siempre, espero que el artículo os haya resultado de utilidad! Y por supuesto, no dudéis en distribuirlo y compartirlo con todo el mundo (pero por favor, citad siempre la fuente original de éste artículo).

Alaaaaaaaaaaaaaaaaaaaaaaa!

Referencias