Saltar al contenido principal

K8s + GlusterFS (CentOS)

· 16 min de lectura
Rei Izumi

Kubernetes tiene varias opciones para crear volúmenes, en otro artículo ya expliqué cómo utilizar NFS, ahora le toca a algo más profesional: GlusterFS.

GlusterFS es un sistema de réplica de datos, similar a crear una RAID a nivel de software, aunque tiene algunos extras, como la capacidad de hacer snapshots o la georeplicación. En caso de Kubernetes, únicamente nos interesa su capacidad para crear réplicas de los datos y compartirlas, similar a una carpeta compartida.

Debo avisar que GlusterFS es realmente grande y Kubernetes no está configurado "por defecto" para utilizarlo (requiere un intermediario: Heketi), así que su configuración es bastante compleja, y además hará falta obtener conocimientos de GlusterFS para gestionarlo manualmente si fuera necesario.

Explicación

Kubernetes es capaz de conectar con servicios externos siempre y cuando estos dispongan de un servicio REST, GlusterFS no dispone de estos, funciona por comandos para gestionar todo, y desde cualquier nodo se puede gestionar al resto.

Por otro lado, tenemos Heketi, que genera una capa por encima de GlusterFS, conecta con cada uno de los nodos para darles ordenes, aun así, Heketi tiene varios problemas:

  • Requiere desplegar un servicio REST, si este servicio falla, ya no se tiene acceso a la gestión del GlusterFS, así que es recomendable tener un clúster de este servicio
  • GlusterFS funciona creando volúmenes, los volúmenes creados manualmente, fuera de Heketi, no pueden ser configurados desde este
  • Heketi requiere una base de datos donde almacenar su información, lo que acaba siendo un problema, queremos solucionar el problema de no perder datos de volúmenes y este acaba teniendo ese mismo problema
  • Existe un proyecto con GlusterFS y Heketi para Kubernetes, que permite desplegar GlusterFS en todos los nodos de Kubernetes, despliega Heketi en clúster dentro del Kubernetes y se encarga de todo, genial ¿verdad? Pues no funciona tan bien como parece.

GlusterFS se recomienda desplegar con al menos 3 nodos, aunque todos se despliegan de la misma forma, con lo que explicaré uno y simplemente habrá que replicar los mismos pasos. En mi caso utilizaré los mismos nodos de Kubernetes como nodos de GlusterFS, aunque estos pasos se pueden utilizar para desplegar el GlusterFS sobre otros servidores.

Utilizaré el Kubernetes desplegado sobre CentOS 8 explicado anteriormente. Sea como sea, todos los nodos deben ser capaces de resolver su nombre, ya sea por tenerlos en un DNS o por añadirlos en el fichero /etc/hosts.

1- Preparar los nodos de GlusterFS

Cada nodo necesita un disco duro sin formatear que será el utilizado por este servicio, en mi caso será he asociado un nuevo disco duro a cada nodo que está en /dev/sdb

El script generado por el proyecto es capaz de desplegar GlusterFS en cada nodo de Kubernetes, pero en mi caso dio muchos errores, además, prefiero hacer este despliegue manualmente para tener un mejor control sobre él.

Por defecto, CentOS dispone de una versión vieja, así que habrá que activar la release propia para acceder a la última versión.

Instalamos y preparamos en cada nodo.

dnf install -y centos-release-gluster
dnf config-manager --set-enabled PowerTools
dnf install -y glusterfs-server
systemctl enable --now glusterd

Abrimos los puertos necesarios para la comunicación de GlusterFS y de Heketi además de activar los componentes que estos necesitan.

firewall-cmd --zone=public --permanent --add-port={2222,24007-24008,49152-49251}/tcp
firewall-cmd --reload
for i in dm_snapshot dm_mirror dm_thin_pool; do modprobe $i; done

A partir de aquí, ya tenemos el servicio preparado, desde cualquier nodo incluiremos al resto y comprobaremos que esté funcionando.

gluster peer probe node2.domain.intranet
gluster peer probe node3.domain.intranet
gluster pool list

Esto nos mostrará todos los nodos asociados.

UUID                                    Hostname                        State
b814471f-ee64-478f-9299-f9124877dd45 node2.moon.intranet Connected
69c1e99d-0015-4065-bc34-a1d80b31f440 node3.moon.intranet Connected
262b1921-5ab9-43b6-bda5-c86ae7c90920 localhost Connected

Esto nos deja los nodos funcionando, este servicio necesita además configurar un volumen para cada proyecto, pero le dejaremos este trabajo a Heketi.

2- Preparando el master de Kubernetes

Antes de empezar: este y los siguientes puntos empiezan a tener bastante complejidad, en algunos casos, un error nos hará eliminar manualmente varios procesos, y no tiene porqué ser fácil (puede requerir conocimientos de GlusterFS o de particiones de Linux), si disponemos de la opción de crear snapshots, será mejor utilizarlos.

El despliegue de Heketi y su preparación se puede hacer desde uno de los scripts del proyecto original, este requiere Git para descargarlo (podemos descargarlo a mano, pero es conveniente tener Git en un master de Kubernetes para poder guardar nuestros ficheros de configuración para cada despliegue) y también python, que el script utilizará para recuperar la lista de nodos.

dnf install -y git python38

CentOS nos creará la opción de python3 y python38, pero el script requiere tener "python", así que creamos un enlace blando para tenerlo disponible, ya que este problema es bastante típico y nos podrá surgir a futuro.

ln -s /usr/bin/python3 /usr/bin/python

Ahora tendremos que descargar el proyecto, pero la versión al escribir este artículo, es únicamente compatible con Kubernetes 1.6, debido a versiones que han desaparecido. En un pull request lo han solucionado para 1.8 (hay otro para 1.7), sí que utilizaré ese para ahorrarme toda la faena.

git clone --single-branch --branch fix-broken https://github.com/pkalever/gluster-kubernetes.git

Lo primero es preparar la topología de nuestro GlusterFS.

cd gluster-kubernetes/deploy/
cp topology.json.sample topology.json
vi topology.json

En este fichero indicamos el nombre del nodo según Kubernetes (kubectl get nodes) en "manage", la IP (NO funciona con nombres de dominio) en "storage" y los discos para Gluster (en mi caso solo tengo /dev/sdb) en "devices".

{
"clusters": [
{
"nodes": [
{
"node": {
"hostnames": {
"manage": [
"temp101"
],
"storage": [
"192.168.1.101"
]
},
"zone": 1
},
"devices": [
"/dev/sdb"
]
},
{
"node": {
"hostnames": {
"manage": [
"temp102"
],
"storage": [
"192.168.1.102"
]
},
"zone": 1
},
"devices": [
"/dev/sdb"
]
},
{
"node": {
"hostnames": {
"manage": [
"temp103"
],
"storage": [
"192.168.1.103"
]
},
"zone": 1
},
"devices": [
"/dev/sdb"
]
}
]
}
]
}

También necesitamos que el master sea capaz de acceder al resto de nodos por SSH sin contraseña, para ello generamos unas claves (nos preguntará la contraseña, dejarla en blanco) y las enviamos a cada nodo.

ssh-keygen -t rsa -b 2048 -f /root/.ssh/id_rsa
ssh-copy-id -i /root/.ssh/id_rsa temp101.domain.internet
ssh-copy-id -i /root/.ssh/id_rsa temp102.domain.internet
ssh-copy-id -i /root/.ssh/id_rsa temp103.domain.internet

Crearemos un namespace para que todo lo relacionado con Heketi y GlusterFS quede separado del resto.

kubectl create namespace glusterfs

Y terminamos con un pequeño detalle, al crear el PersistentVolume, tendremos que indicar una URL que el master debe ser capaz de resolver fuera de Kubernetes, por si mismo, así que o publicamos este REST a través de Ingress o similar, o utilizamos el nombre interno que genera Kubernetes para acceder a los deployments (<service>.<namespace>.svc.cluster.local), este último es el más práctico pero no es accesible por defecto, así que habrá que modificarlo.

vi /etc/kubernetes/manifests/kube-controller-manager.yaml

Añadimos la siguiente línea encima de "hostNetwork: true", nos quedará así:

dnsPolicy: ClusterFirstWithHostNet
hostNetwork: true

Reiniciamos el kubelet para aplicar este cambio.

systemctl restart kubelet

Debo avisar que este cambio permite a los servicios como un PersistentVolume utilizar los DNS internos de Kubernetes, pero nuestro servidor seguirá sin poderlos utilizar.

3- Desplegar Heketi

Ya tenemos todo lo necesario para desplegar Heketi, crear un volumen de GlusterFS donde almacenar su base de datos y hacer que Heketi se despliegue totalmente en Kubernetes utilizando ese volumen, todo esto se puede hacer con el script que viene en el proyecto descargado por Git.

Cambiar los user-key y admin-key por vuestra contraseña.

Antes de ejecutarlo, este proceso es muy complejo de revertir, es el momento ideal para un snapshot.

./gk-deploy \
--user-key heketi \
--admin-key heketi \
--ssh-keyfile /root/.ssh/id_rsa \
--ssh-user root \
--cli kubectl \
--no-object \
--templates_dir ./kube-templates \
--namespace glusterfs \
topology.json

Si todo ha ido bien, al terminar nos mostrará los pasos a seguir, aunque en la versión actual los logs son algo ilegibles, pero encontraremos algo como:

heketi is now running and accessible via http://10.244.227.197:8080 . To run
administrative commands you can install 'heketi-cli' and use it as follows:

# heketi-cli -s http://10.244.227.197:8080 --user admin --secret 'heketi' cluster list

You can find it at https://github.com/heketi/heketi/releases . Alternatively,
use it from within the heketi pod:

# kubectl -n glusterfs exec -i heketi-68ffb8d4b5-b9gk9 -- heketi-cli -s http://localhost:8080 --user admin --secret 'heketi' cluster list

Podemos confirmar el funcionamiento con la web de hello de Heketi.

curl http://10.244.227.197:8080/hello

Y ver todo lo desplegado en el nuevo namespace.

kubectl get all -n glusterfs

Este es mi resultado.

NAME                          READY   STATUS    RESTARTS   AGE
pod/heketi-68ffb8d4b5-b9gk9 1/1 Running 0 7m18s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/heketi ClusterIP 10.99.152.1 <none> 8080/TCP 7m19s
service/heketi-storage-endpoints ClusterIP 10.111.76.220 <none> 1/TCP 7m26s

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/heketi 1/1 1 1 7m19s

NAME DESIRED CURRENT READY AGE
replicaset.apps/heketi-68ffb8d4b5 1 1 1 7m19s

Si todo es correcto, ¡genial! no es nada fácil llegar a hacerlo funcionar (en serio, no lo es ...), pero aún no hemos acabado.

Tal como indica, se necesita tener el heketi-cli o utilizar el contenedor como intermediario, para tener más facilidad yo prefiero descargarlo y así tenerlo más a mano.

Actualmente tenemos 2 opciones, o descargar únicamente el cliente, o tener todo el paquete con ejemplos, estos comandos descargan ambos para que cada cuál pueda elegir, si alguno no te interesa, siempre lo puedes borrar.

cd /root
dnf -y install wget
curl -s https://api.github.com/repos/heketi/heketi/releases/latest \
| grep browser_download_url \
| grep linux.amd64 \
| cut -d '"' -f 4 \
| wget -qi -
for i in `ls | grep heketi | grep .tar.gz`; do tar xvf $i; done
sudo cp ./heketi-client/bin/heketi-cli /usr/local/bin

Ahora ya tendremos acceso al cliente de heketi, podemos confirmarlo.

heketi-cli --version

Cada comando que utilicemos habrá que pasarle la URL, usuario y contraseña, pero podemos declarar las variables (se pierden al cerrar la sesión, aunque podemos definirlas de otras formas para mantenerlas u ocultar mejor la contraseña).

export HEKETI_CLI_SERVER=$(kubectl get svc/heketi -n glusterfs --template 'http://{{.spec.clusterIP}}:{{(index .spec.ports 0).port}}')
export HEKETI_CLI_USER=admin
export HEKETI_CLI_KEY=heketi

Ya podremos conectar, estos son algunos de los comandos y respuestas típicas.

heketi-cli cluster list

Resultado:

Clusters:
Id:fa99c84472256051fc07b16203003653 [file][block]

Información del clúster.

heketi-cli cluster info fa99c84472256051fc07b16203003653

Resultado:

Cluster id: fa99c84472256051fc07b16203003653
Nodes:
0161cae81231a04f4ae0f5425ce406d5
4fcbf4f80645e74d0b65fe98c5904993
d968f2c17bfbda5b29c5d49a2332eec4
Volumes:
c3afbd040a6cb35b317b6a1436353def
Block: true

File: true

Información de nodos.

heketi-cli node list

Resultado:

Id:0161cae81231a04f4ae0f5425ce406d5     Cluster:fa99c84472256051fc07b16203003653
Id:4fcbf4f80645e74d0b65fe98c5904993 Cluster:fa99c84472256051fc07b16203003653
Id:d968f2c17bfbda5b29c5d49a2332eec4 Cluster:fa99c84472256051fc07b16203003653

Topología.

heketi-cli topology info

Quizás el más importante sea la lista de volúmenes.

heketi-cli volume list

Resultado:

Id:c3afbd040a6cb35b317b6a1436353def    Cluster:fa99c84472256051fc07b16203003653    Name:heketidbstorage

También podemos utilizar los comandos desde el contenedor.

kubectl exec -i heketi-68ffb8d4b5-b9gk9 -n glusterfs -- heketi-cli --user admin --secret heketi -s http://localhost:8080 cluster list

4- Añadir servicio

Necesitamos definir un StorageClass que hará de base, todos los PersistentVolumeClaim irán asociados a él, en mi caso le asociaré un secret a la contraseña, si indicáramos la contraseña visible como otro parámetro podríamos unificar el storageclass dentro del namespace de glusterfs y reutilizarlo en todos los sitios, pero con un secret de por medio únicamente he logrado errores, así que en cada namespace se generará el storageclass y su secret, en mi caso lo dejaré en default.

Definimos el gluster-storageclass.yaml.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: glusterfs-storage
provisioner: kubernetes.io/glusterfs
allowVolumeExpansion: true
reclaimPolicy: Retain
parameters:
resturl: "http://heketi.glusterfs.svc.cluster.local:8080"
restauthenabled: "true"
restuser: "admin"
secretNamespace: "default"
secretName: "heketi-secret"

Para utilizar esto necesitamos crea un secret, primero prepararemos la contraseña.

echo -n "heketi" | base64

Que utilizaremos para crear el secret, heketi-secret.yaml.

apiVersion: v1
kind: Secret
metadata:
name: heketi-secret
namespace: default
data:
key: aGVrZXRp
type: kubernetes.io/glusterfs

Con esto ya podemos definirlo dentro del namespace de glusterfs.

kubectl apply -f gluster-storageclass.yaml
kubectl apply -f heketi-secret.yaml

Podemos confirmar que se ha creado.

kubectl get storageclass

Resultado:

NAME                PROVISIONER               RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
glusterfs-storage kubernetes.io/glusterfs Retain Immediate true 7s

5- Desplegar Nginx en GlusterFS

Ya podemos crear un PersistentVolumeClaim (PVC) para que lo utilice Nginx, gluster-pvc-nginx.yaml.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gluster-nginx
annotations:
volume.beta.kubernetes.io/storage-class: glusterfs-storage
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi

Al crear el PVC, se genera el volumen con Heketi, así que lo crearemos y confirmamos que todo funcione antes de utilizarlo.

kubectl apply -f gluster-pvc-nginx.yaml

El PVC contendrá el volumen de GlusterFS y será el que utilizaremos para Nginx.

kubectl get pvc

Resultado:

NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS        AGE
gluster-nginx Bound pvc-e92993a2-5c38-4290-972d-e0fd55e0190f 1Gi RWX glusterfs-storage 4s

Si ha funcionado, también nos habrá creado el PV.

kubectl get pv

Resultado:

NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                   STORAGECLASS        REASON   AGE
pvc-e92993a2-5c38-4290-972d-e0fd55e0190f 1Gi RWX Retain Bound default/gluster-nginx glusterfs-storage 49s

Ahora ya podemos desplegar el Nginx, nginx-deployment.yaml.

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
volumeMounts:
- name: gluster-storage
mountPath: "/usr/share/nginx/html"
volumes:
- name: gluster-storage
persistentVolumeClaim:
claimName: gluster-nginx

Desplegamos y revisamos que los pods se desplieguen.

kubectl apply -f nginx-deployment.yaml
kubectl get pods -l app=nginx

6- Test y gestión de Heketi/GlusterFS

Con Nginx ejecutado sobre un nuevo volumen de GlusterFS, queda hacer las pruebas y entender cómo gestionar todo esto.

Podemos mostrar la lista de volúmenes de Heketi.

heketi-cli volume list

Nos mostrará el volumen con la base de datos de Heketi y el nuevo.

Id:859c773486f43186c95f35e67aab16f3    Cluster:fa99c84472256051fc07b16203003653    Name:vol_859c773486f43186c95f35e67aab16f3
Id:c3afbd040a6cb35b317b6a1436353def Cluster:fa99c84472256051fc07b16203003653 Name:heketidbstorage

Perfecto, tenemos el identificador de Heketi, el name corresponde a su mismo valor en GlusterFS, pero ¿qué pasa cuando tengamos 5? No hay forma de saber qué volumen de Kubernetes corresponde a cuál en GlusterFS. Para resolver esto primero hay que entender un poco más de cómo funciona todo esto.

Al crear un PVC, este llama a Heketi para crear un nuevo volumen y que él se encargue de crearlo en los nodos de GlusterFS, Heketi genera un Id para el nuevo volumen y lo devuelve, él no almacena nada externo, para solucionar esto, el PV creado por el PVC sí que almacena el identificador de Heketi (que además necesitará si, por ejemplo, quisiéramos hacer modificaciones del volumen), debido a que es un dato especial, con un get no podemos extraerlo, pero se puede conseguir con una template.

kubectl get pv -o 'go-template={{range .items}}name={{.metadata.name}} claimName={{.spec.claimRef.name}} heketiName={{.spec.glusterfs.path}}{{"\n"}}{{end}}'

Este nos devuelve el Id del volumen de Kubernetes, el nombre de nuestro PVC y el nombre del volumen en GlusterFS.

name=pvc-e92993a2-5c38-4290-972d-e0fd55e0190f claimName=gluster-nginx heketiName=vol_859c773486f43186c95f35e67aab16f3

Con esto podemos ir a uno de los nodos y mirar el listado de volúmenes.

gluster volume list

Ahora sí, podemos identificar el volumen real.

heketidbstorage
vol_859c773486f43186c95f35e67aab16f3

Y, con ello, extraer toda la información del volumen.

gluster volume info vol_859c773486f43186c95f35e67aab16f3

Por defecto todos los volúmenes son Replica 3, y podemos ver en qué rutas está exactamente.

Volume Name: vol_859c773486f43186c95f35e67aab16f3
Type: Replicate
Volume ID: 0e71dcd2-f61b-4c3b-bc3f-28cbdb068d0b
Status: Started
Snapshot Count: 0
Number of Bricks: 1 x 3 = 3
Transport-type: tcp
Bricks:
Brick1: 192.168.1.101:/var/lib/heketi/mounts/vg_9c3e257deb0838270b381143e13967bd/brick_333a40b9e79cb3f21c8bea92c37f79cc/brick
Brick2: 192.168.1.102:/var/lib/heketi/mounts/vg_0809f47b291fc6fa26987242a220208f/brick_9078029f46a1fca8ac744a8f0671f130/brick
Brick3: 192.168.1.103:/var/lib/heketi/mounts/vg_b415991f501d9c99d7281aa32210bc86/brick_5855346072f62e0bdd5612fa3dc5f9c5/brick
Options Reconfigured:
user.heketi.id: 859c773486f43186c95f35e67aab16f3
storage.fips-mode-rchecksum: on
transport.address-family: inet
nfs.disable: on
performance.client-io-threads: off

Ya tenemos lo suficiente para hacer pruebas, así que accedemos a uno de los pods, mirando en qué nodo está, y nos vamos a un nodo diferente para acceder a su ruta de montaje para crear y listar ficheros desde nodos diferentes y confirmar que la réplica está funcionando.

kubectl get pods -o wide -l app=nginx
kubectl exec -it nginx-deployment-68fb6b7489-cbz5n -- bash

Aquí creamos un fichero nuevo y comprobamos que nos aparece.

cd /usr/share/nginx/html
echo "Hello from GlusterFS" > index.html
curl localhost

Si desde un nodo vamos a la ruta que nos indica el volumen y mostramos los ficheros, encontraremos el index.html

cd /var/lib/heketi/mounts/vg_0809f47b291fc6fa26987242a220208f/brick_9078029f46a1fca8ac744a8f0671f130/brick
ls

Si seguimos probando, veremos que podemos crear un fichero en el nodo y este será mostrado por Nginx, aun así, debo advertir que las réplicas no son inmediatas, y, curiosamente, habrá veces que curl sea capaz de mostrar ficheros que con ls aun no vemos.

7- Borrar volumenes

Cuando borramos un PVC, este borrará el PV, pero no borrará ni el volumen en Heketi ni en GlusterFS, y debido a que la asociación con estas particiones es dinámica, cuando creamos un nuevo PVC, este creará un nuevo volumen en vez de reutilizar uno anterior, así que ojo con ello, ya que con esta configuración nunca podremos reenganchar con un volumen ya creado si perdemos su PVC.

Por esta misma regla, debido a que utilizamos el PV para identificar qué volumen de Kubernetes corresponde a qué volumen en GlusterFS, si lo borramos sin mirar antes la correspondencia, nos será más difícil de encontrar el volumen a borrar.

También hay que tener en cuenta, que la gestión de volúmenes se hace desde Heketi, si borramos un volumen directamente en GlusterFS, Heketi no lo sabrá y nos dará información errónea.

Borrar requiere un orden, primero debemos utilizar el comando explicado previamente para obtener los Ids de los volúmenes a cada lado.

kubectl get pv -o 'go-template={{range .items}}name={{.metadata.name}} claimName={{.spec.claimRef.name}} heketiName={{.spec.glusterfs.path}}{{"\n"}}{{end}}'

De este podemos extraer la siguiente información:

  • name=pvc-e92993a2-5c38-4290-972d-e0fd55e0190f: Nombre del PV a borrar, si reemplazamos "pvc-" por "glusterfs-dynamic-", obtendremos el service a borrar
  • claimName=gluster-nginx: Nombre del PVC a borrar
  • heketiName=vol_859c773486f43186c95f35e67aab16f3: Si eliminamos el "vol_", obtenemos el Id necesario para borrar el volumen

Con estos datos procedemos a borrar toda la información del volumen en todos los lugares.

kubectl delete pvc/gluster-nginx pv/pvc-e92993a2-5c38-4290-972d-e0fd55e0190f services/glusterfs-dynamic-e92993a2-5c38-4290-972d-e0fd55e0190f
heketi-cli volume delete 859c773486f43186c95f35e67aab16f3

La destrucción del volumen en Heketi, también destruirá los datos en GlusterFS, así que cuidado con ello.

Final

Llegar hasta aquí no es nada fácil, y mucho me temo que en próximas versiones, algunos de estos pasos pueden dejar de funcionar como ha ido pasando en cada versión de Kubernetes.

También, el despliegue de Heketi dentro de Kubernetes nos hace la vida mucho más sencillo, pudiendo crear automáticamente PVC y que estos se encarguen de todo, si utilizaramos GlusterFS y lo compartiéramos con otros métodos, nos obligaría a tener que crear manualmente los volúmenes, o a utilizar los comandos de Heketi para hacerlo manualmente, esto no es algo malo, simplemente es más lento y puede entorpecer a algunos sistemas, sobretodo a aquellos más automatizados, es nuestra elección decidir qué preferimos.