Hoy vamos a poner la guinda en el pastel de la serie de entradas sobre el mundo de los microservicios, esto es, los contenedores, hablando de alta disponibilidad con Kubernetes. Os explicaré cómo construir un entorno de alta disponibilidad para desplegar nuestros contenedores.

Para ello utilizaremos nuestro proveedor de confianza: Clouding.io. Mi intención es explicar paso a paso el montaje. Nos serviremos, para realizar el proceso, de un nodo «master» y dos «workers«.

Digamos que el nodo master cumple el rol de director de orquesta y los workers son miembros de esta y actúan según aquél les indica. Evidentemente, en entornos productivos con gran volumen esta cantidad de participantes no sería suficiente. Quizá entonces serían tres masters y cinco workers o aún más (sí, lo recomendado es que los nodos siempre sean impares). Lo mismo sucede con el número de núcleos de procesador y memoria.

En lo que respecta al disco, en el caso de Clouding.io disfrutamos de discos sólidos SSD. Y, por último, que no menos importante, en lo que respecta a la red también se nos garantiza una Cisco de 20 Gbps.

Si has llegado hasta aquí y no sabes en qué consisten los contenedores y los microservicios, te recomiendo que eches un vistazo a esta entrada y a todas las que vinieron después.

Crear un entorno de alta disponibilidad con Kubernetes

Antes de nada, listemos lo que vamos a necesitar. Lo primero es un nodo master y dos workers. Para ello vamos a utilizar una distribución GNU/Linux, concretamente Debian 10, aprovechando que recientemente han añadido esta distribución a nuestro proveedor, Clouding.io.

La creación de los VPS no tiene excesivo misterio desde el panel de administración de nuestro proveedor. Eso sí, le asignaremos a cada nodo 4 GB de memoria (aunque con 2 GB sería más que suficiente para empezar) y 2 núcleos de procesador.

Una vez creados los tres hosts contamos con los siguientes datos:

  • servmaster.bitsandlinux.com (Nodo master)
  • servworker01.bitsandlinux.com (Nodo worker 1)
  • servworker02.bitsandlinux.com (Nodo worker 2)

Asignaremos 20 GB de espacio de disco a cada uno.  Punto importante, cuando tengamos las máquinas creadas, es realizar el correspondiente intercambio de llaves entre sí, para facilitar el montaje y la configuración.

Además vamos a rizar el rizo, como se suele decir, y utilizaremos un cuarto host como nodo de almacenaje, donde compartiremos un recurso NFS. Para ello utilizaremos un pequeño nodo también con Debian 10, pero que tenga suficiente disco. En esta entrada no daré muchos detalles al respecto pero, para montar el nodo NFS, os podéis servir de esta entrada que escribí en su día:  Compartiendo carpetas en red con NFS.

Definiendo el entorno

Lo primero que haremos, antes de empezar a instalar los paquetes necesarios, es igualar el fichero /etc/hosts para todos los nodos, quedando así:

# Kubernetes
	
185.166.214.124 servmaster.bitsandlinux.com servmaster
93.189.88.105 servworker01.bitsandlinux.com servworker01
93.189.94.83 servworker02.bitsandlinux.com servworker02
85.208.21.25 servstorage.bitsandlinux.com servstorage

En el caso de Clouding.io debemos modificar otro fichero, ya que los cambios realizados en /etc/hosts/ se perderán al reiniciar. Para evitarlo debemos editar el fichero /etc/cloud/templates/hosts.debian.tmpl.

Además, en los nodos habilitamos un usuario operador con el nombre que queramos y lo añadimos al grupo sudo (wheel en el caso de Centos).

useradd -c "Usuario operador" -d "/home/operador" \
-s "/bin/bash" -G sudo operador
# Y definimos una contraseña
mkdir /home/operador
chown operador:operador /home/operador
passwd operador

Deshabilitamos el acceso para el usuario root para mejorar la seguridad:

sed -i 's/PermitRootLogin yes/PermitRootLogin no/g' \
/etc/ssh/sshd_config

Reiniciamos el servicio y continuamos:

sudo systemctl restart sshd

Y, para facilitar el acceso entre los hosts, creamos con ssh-keygen una llave pública y privada para ssh, añadiendo el intercambio de llaves con ssh-copy-id entre los hosts.

Deshabilitando SELinux

En muchos casos, SELinux está habilitado por defecto, pero no nos interesa. Debemos deshabilitarlo como sigue:

sudo sed -i 's/SELINUX=enabled/SELINUX=disabled/g' \
/etc/selinux/config

Para que este cambio tenga efecto debemos reiniciar el host:

sudo reboot

Configurar el cortafuegos

Si tenemos el cortafuegos habilitado (véase firewalld en el caso de Centos) debemos abrir los siguientes puertos:

firewall-cmd --permanent --add-port=6443/tcp
firewall-cmd --permanent --add-port=2379-2380/tcp
firewall-cmd --permanent --add-port=10250/tcp
firewall-cmd --permanent --add-port=10251/tcp
firewall-cmd --permanent --add-port=10252/tcp
firewall-cmd --permanent --add-port=10255/tcp
firewall-cmd –reload

Expliquemos en qué consiste cada uno:

Puerto o rangoProtocoloFunciónDirecciónServicios que lo utilizan
6443TCPAPI de KubernetesEntranteTodos
2379-2380TCPServicios etcdEntrantekubi-apiserver, etcd
10250TCPKubelet APIEntranteSelf, control plane
10251TCPkube-schedulerEntranteSelf
10252TCPkube-controller-managerEntranteSelf

En el caso de nuestro proveedor de confianza solo debemos añadir los puertos correspondientes al cortafuegos que tengamos definido desde el panel de administración.

Habilitar e instalar Docker-CE

Ahora nos toca habilitar el repositorio comunitario de Docker en los tres hosts, tanto en el manager como en los workers. Para no ir uno a uno podemos utilizar herramientas como pssh para facilitar nuestro trabajo, aprovechando que tenemos el intercambio de llaves hecho. En mi caso, he creado un fichero con información de todos los nodos, para interactuar a la vez con todos ellos. Al final, como todo, esto es a gusto del consumidor.

Antes de nada instalamos una serie imprescindible para el buen funcionamiento del entorno:

sudo apt update
sudo apt install apt-transport-https ca-certificates 
sudo apt install curl gnupg2 software-properties-common

Para habilitar el repositorio en los nodos primero añadimos la llave correspondiente:

curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -

Y luego el repositorio:

sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"

Nos aseguramos de utilizar el repositorio de Docker y no el de Debian:

sudo apt update
apt-cache policy docker-ce

Ya podemos realizar la instalación:

sudo apt install docker-ce

Asegurarse de que iptables no use el backend de nftables

No hace mucho os hablé de nftables y cómo era el reemplazo en muchas distribuciones para los sistemas clásicos de cortafuegos como iptables. El problema es que este sistema no es compatible con las últimas versiones de los paquetes de administración de Kubernetes, como kubeadm, porque causa reglas de firewall duplicadas y rompe kube-proxy.

En Debian 10, que es la distribución que estamos utilizando, nftables está habilitado como backend para iptables, por lo que tendremos que hacer algunos cambios (pasar el sistema al modo heredado). Esto me ha causado muchos quebraderos de cabeza ya que, inicialmente, quería utilizar Centos 8 para esta entrada y no es compatible, justamente por esto.

Para habilitar el modo heredado (legacy), seguimos estos pasos:

# Nos aseguramos que los binarios heredados estén instalados
sudo apt-get install -y iptables arptables ebtables
# Cambiamos a versiones legacy
sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
sudo update-alternatives --set arptables /usr/sbin/arptables-legacy
sudo update-alternatives --set ebtables /usr/sbin/ebtables-legacy

Instalar Kubernetes

Debemos habilitar el correspondiente repositorio de la siguiente manera, en el nodo con el rol de master y en los workers:

sudo vi /etc/apt/sources.list.d/kubernetes.list

Y añadimos:

deb https://apt.kubernetes.io/ kubernetes-xenial main

El paquete Kubeadm nos ayuda a construir nuestro entorno de alta disponibilidad. Además, también es compatible con otras funciones del ciclo de vida del clúster, como actualizaciones, degradación y administración de tokens de arranque. Y también es compatible con la integración con otras herramientas de orquestación, como Ansible o Terraform.

Para poder utilizar el repositorio debemos añadir su correspondiente llave:

curl -s \
https://packages.cloud.google.com/apt/doc/apt-key.gpg \
| sudo apt-key add -

Pasa a instalarlo:

sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

Utilizar cgroup para kubelet

Al usar Docker, kubeadm detectará automáticamente el controlador cgroup para el kubelet y lo configurará en el archivo /var/lib/kubelet/kubeadm-flags.env durante el tiempo de ejecución.

Si estamos utilizando un CRI diferente, debe modificar el archivo /etc/default/kubelet (/etc/sysconfig/kubelet para CentOS, RHEL, Fedora) con el valor de su controlador cgroup, así:

KUBELET_EXTRA_ARGS=--cgroup-driver="value"

Este archivo será utilizado por kubeadm init y kubeadm join para generar argumentos adicionales definidos por el usuario para el kubelet. Solo debemos hacerlo si el controlador cgroup de su CRI no es cgroupfs, porque ese es el valor predeterminado en el kubelet.

Si hacemos algún cambio debemos reiniciar el daemon:

systemctl daemon-reload
systemctl restart kubelet

Iniciar el nodo master

El nodo con el rol de master ejecuta algunos servicios esenciales para el clúster. Antes de iniciarse realizará una serie de comprobaciones previas, esenciales para saber si el nodo cumple con todos los requisitos. Estas comprobaciones previas exponen advertencias y sobresalen en caso de errores. kubeadm init luego descarga e instala los componentes del plano de control del clúster.

Pero antes de hacer nada debemos deshabilitar la memoria transaccional o SWAP.

sudo swapoff -a

Iniciamos Kubernetes con la siguiente orden:

sudo kubeadm init --pod-network-cidr=10.244.0.0/16

Si todo ha ido bien se nos indicarán las instrucciones para añadir el primer nodo master para, más adelante, añadir los nodos con el rol de worker. Es importante apuntar los datos que nos ofrece.

Seguimos las instrucciones, en este caso para el usuario root:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

Comprobamos que el nodo se ha incorporado correctamente al entorno:

[email protected]:/etc/selinux$ kubectl get nodes
NAME         STATUS     ROLES    AGE     VERSION
servmaster   NotReady   master   5m12s   v1.17.22

El mensaje de estado «NotReady» es normal, ya que todavía no hemos generado la red de pod para el entorno.

La red de pod es la red superpuesta para el clúster, que se implementa en la parte superior de la red de nodo actual. Está diseñado para permitir la conectividad a través del pod.

Crear y configurar la red pod

La implementación del clúster de red es un proceso muy flexible que depende de nuestras necesidades y hay muchas opciones disponibles. Para este tutorial utilizaremos una configuración simple: Flannel. Es una red superpuesta que satisface los requisitos de Kubernetes.

Estos son los comandos indispensables para obtener la configuración de la red de pod:

kubectl apply -f \
https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

Con el resultado:

podsecuritypolicy.policy/psp.flannel.unprivileged created
clusterrole.rbac.authorization.k8s.io/flannel created
clusterrolebinding.rbac.authorization.k8s.io/flannel created
serviceaccount/flannel created
configmap/kube-flannel-cfg created
daemonset.apps/kube-flannel-ds-amd64 created
daemonset.apps/kube-flannel-ds-arm64 created
daemonset.apps/kube-flannel-ds-arm created
daemonset.apps/kube-flannel-ds-ppc64le created
daemonset.apps/kube-flannel-ds-s390x created

Ahora sí, el nodo master ya debería estar activo:

[email protected]:/etc/selinux$ kubectl get nodes
NAME         STATUS   ROLES    AGE   VERSION
servmaster   Ready    master   12m   v1.17.2

Bueeeno, ya está, pasemos a trabajar con los nodos worker.

Añadir los nodos worker

Recordar que en los nodos hosts worker ya hemos configurado el fichero /etc/hosts, deshabilitado SELinux y habilitado los puertos en el cortafuegos. Además hemos instalado los paquetes necesarios, tanto docker como kubeadm.

Utilizando la línea que se nos indicó anteriormente, desde cada nodo worker nos conectamos con el master:

sudo su -
kubeadm join ip:puerto \
--token codigo-de-token \
--discovery-token-ca-cert-hash \ 
sha256:6db57220b2c4cecf84526a969d960501e6d1ecefa9f4980a736a2322cff8ca27

Una vez añadidos los nodos, desde el master podemos ver que, efectivamente, se han añadido:

[email protected]:~$ kubectl get nodes
NAME           STATUS   ROLES    AGE   VERSION
servmaster     Ready    master   42m   v1.17.2
servworker01   Ready       11m   v1.17.2
servworker02   Ready       24s   v1.17.2

Y voilà, ya tenemos montado nuestro entorno de alta disponibilidad con HA en Kubernetes.

No hace falta que diga que esto es una guía inicial y seguro que me he dejado muchas cosas. Además, en entornos productivos un solo master es un punto de fallo muy importante. Lo ideal es, por lo menos, tres nodos master y cinco workers.

Empezar a trabajar con Kubernetes

A modo de muestra vamos a desplegar un contenedor con el servidor web Nginx para poder mostrar su funcionamiento.

Pero antes vamos a montar en el nodo master el recurso NFS que hemos creado al principio de la entrada en el nodo de storage. Bien, creamos la carpeta donde se montará el sistema de ficheros:

sudo mkdir /srv/storage

Y añadimos la línea correspondiente en /etc/fstab:

sudo vi /etc/fstab

Con el contenido:

servstorage.bitsandlinux.com:/srv/storage /srv/storage  nfs     defaults    0 0

Y montamos:

sudo mount -a

Accedemos a la carpeta /serv/storage/ y creamos el fichero en la ruta para almacenar los ficheros del nginx:

cd /srv/storage
mkdir nginx
cd nginx

Aquí creamos nuestro fichero en formato YAML con el nombre que queramos:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
       - name: nginx
         image: nginx
         ports:
         - containerPort: 80

Para validar que todo ha ido bien y la información está bien formateada podemos utilizar esta herramienta en línea jsonformatter.org.

Creamos un nuevo recurso utilizando el fichero YAML:

kubectl create -f nginx.yaml

Con un mensaje similar a este al crear el recurso:

deployment.apps/nginx-deployment created

Podemos mostrar la información de todo lo desplegado con el comando kubectl get deployments:

NAME               READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   1/2     2            1           6m39s
nginx-server       1/1     1            1           20h
redis-master       1/1     1            1           20h

Y si queremos mostrar más información:

kubectl describe deployment nginx-deployment

Por último, que no menos importante, mostramos información sobre los pods existentes:

NAME                                READY   STATUS              RESTARTS   AGE
nginx-deployment-54f57cf6bf-8zt8m   0/1     ContainerCreating   0          8m52s
nginx-deployment-54f57cf6bf-xrz6l   1/1     Running             0          8m52s
nginx-server-7d9b6b687c-kd877       1/1     Running             0          19h
redis-master-7db7f6579f-f9kd9       1/1     Running             0          20h

También podemos obtener información de un pod en concreto:

kubectl describe pod nginx-deployment-54f57cf6bf-xrz6l

Para terminar, en esta parte es necesario crear un servicio. Si no, no veremos el resultado de nuestro servidor HTTP.

Creamos el fichero YAML correspondiente para el servicio:

apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
  labels:
    run: nginx-svc
spec:
  type: NodePort
  ports:
  - port: 80
    protocol: TCP
  selector:
    app: nginx

Ídem que en el apartado anterior: es importante seguir el formato correcto para el fichero.

Y creamos el servicio:

kubectl create -f nginx-svc.yaml

Podemos listar los servicios existentes:

[email protected]:~/yaml$ kubectl get svc
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1              443/TCP        18m
nginx-svc    NodePort    10.106.79.19           80:30732/TCP   102

Consultamos en nuestro navegador web preferido la dirección del nodo master más el puerto:

Y esto es todo, creo que la entrada ya ha quedado suficientemente extensa.

¿Qué os ha parecido? ¿Tenéis experiencia con entornos de alta disponibilidad con contenedores? ¿Sois de Docker Swarm o de Kubernetes? Podéis dejar vuestras respuestas en los comentarios 🙂


Derechos de la imagen de portada en Pixabay.


Fuentes consultadas

Kubernetes – Cluster Networking
Kubernetes – Installing kubeadm
Clouding.io – Introducción a Kubernetes
Github – Coreos Flannel

avatar

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

  Subscribe  
Notificarme de