Déploiement d'un cluster consul

Cette étape est assez courte, mais nécessite d’être bien organisé. Encore une fois je vais m’inspirer d’un tutorial trouvé pour l’occasion.

Je vais cependant m’en détacher sur certains points et m’en servir comme base de travail

 

Introduction sur Consul

Le but est de déployer « Consul » sur le cluster Kubernetes.

 

Consul est un logiciel open source maintenu par la société « hashicorp ». Il permet de gérer une base clef/valeur qui peut être utilisée pour différents usages. Dans mon cas je souhaite m’en servir comme un moyen de configurer dynamiquement et simplement « Traefik » qui sera déployé dans l’étape suivante.

 

« Consul » peut être utilisé seul ou en cluster. En production, c’est souvent la solution cluster qui est choisie pour assurer une haute disponibilité. Dans ce cas, il faut un minimum de trois nœuds. Le protocole de consensus "raft" qui est utilisé pour élire un leader parmi les nœuds à besoin de ce nombre minimal d’instances pour fonctionner normalement. Les données sont répliquées sur toutes les instances mais une seule est désignée comme leader et fait office de référence. Si celle-ci vient à tomber alors les instances restantes élisent un nouveau maitre. N'hésitez pas à jeter un oeuil à ce site qui explique parfaitement le principe.

 

« Consul » a l’avantage par rapport à d’autres solutions fournissant le même type de service d’avoir en plus d’un pilotage en ligne de commande, une interface graphique bien utile pour intégrer de nouvelles valeurs depuis un simple navigateur internet.

 

Déploiement d'un cluster Consul sous Kubernetes

Création d'un namespace

 

Je débute par  la création d'un namespace kubernetes dédié pour héberger l’application. De ce que j’ai compris un namespace Kubernetes permet d’isoler des ressources entres elles et de créer des environnements de travail cloisonnés, par exemple un namespace de développement et un namespace de production. Cela autorise d’exploiter un seul et même cluster kubernetes pour différentes populations sans risque de voir leurs objets s’interférer entre eux, avec la possibilité d’établir des droits d’accès et d’administration différents par namespace. C'est pour moi une bonne pratique de travailler dans des namespace distincts plutôt que d’exploiter celui par défaut.

 

kubectl create namespace prd-lan-coolcorp

Maintenant que mon namespace est créé, je dois, soit préciser son nom dans les fichiers yaml que j’utilise, soit l’ajouter en argument de mes lignes de commandes kubectl. On peut d’ailleurs modifier la configuration de kubectl pour changer son namespace par défaut. Par exemple dans mon cas:

kubectl config set-context --current --namespace= prd-lan-coolcorp

Organisation des fichiers et prérequis

La question de namespace étant traitée, je vais à l’image du tutoriel dont je m’inspire créer mes dossiers de travails, via la logique suivante :

Organisation des fichiers

Sécurisation des communications Consul

Pour sécuriser la communication entre les instances “Consul”, je génère une autorité de certification pour autosigner des certificats à associer à Consul.

openssl genrsa -out certs/ca/ca.key 2048

openssl req -x509 -new -nodes -key certs/ca/ca.key -subj "/CN=cluster.local" -days 10000 -out certs/ca/ca.crt

Il semble important d’utiliser comme "Canonical Name" le nom cluster.local qui correspond au domaine par défaut du DNS interne Kubernetes

Je m’occupe maintenant du certificat pour consul. Je génère d’abord une clef "consul-key.key"

 

openssl genrsa -out certs/consul/consul-key.key 2048

Je prépare ensuite la demande de génération du certificat avec le fichier de réponses “csr.conf” que je place dans cert/consul.

[ req ]

default_bits = 2048

prompt = no

default_md = sha256

req_extensions = req_ext

distinguished_name = dn

 

[ dn ]

C = FR

ST = IDF

L = PARIS

O = COOLCORP

OU = IT

CN = server.lan.cluster.local

 

[ req_ext ]

subjectAltName = @alt_names

 

[ alt_names ]

DNS.1 = server.lan.cluster.local

IP.1 = 127.0.0.1

 

[ v3_ext ]

authorityKeyIdentifier=keyid,issuer:always

basicConstraints=CA:FALSE

keyUsage=keyEncipherment,dataEncipherment

extendedKeyUsage=serverAuth,clientAuth

subjectAltName=@alt_names

Le "Cannocical Name" ainsi que l’ "Alternative Name" (DNS.1) doivent correspondre. Je n’ai pour l’instant pas réussi à mettre autre chose que "server" comme nom de base. "lan" correspond à l’identifiant du site que j’ai choisi pour mon cluster consul. La suite reprend le domaine dns par défaut du cluster Kubernetes.

Je génère le CSR

openssl req -new -key certs/consul/consul-key.key -out certs/consul/consul.csr -config certs/consul/csr.conf

Je soumets mon CSR à ma CA pour récupérer le certificat

openssl x509 -req -in certs/consul/consul.csr -CA certs/ca/ca.crt -CAkey certs/ca/ca.key -CAcreateserial -out certs/consul/consul.crt -days 10000 -extensions v3_ext -extfile certs/consul/csr.conf

Génération du certificat

À l’aide du binaire consul, je génère une clef de chiffrement avec la commande "consul keygen"

Clef consul

Je vais temporairement utiliser cette clef comme variable d’environnement depuis la console qui va me service à générer mon secret dans Kubernetes.

 

Set de la clef consul

 

Ce secret va me permette de stocker la clef consul de chiffrement ainsi que les certificats générés précédemment.

 

kubectl create secret generic sec-consul-traefik-lan --from-literal="gossip-encryption-key=${GOSSIP_ENCRYPTION_KEY}" --from-file=certs/ca/ca.crt --from-file=certs/consul/consul.crt --from-file=certs/consul/consul-key.key --namespace=prd-lan-coolcorp

Il faut maintenant préparer la configuration du cluster Consul via le fichier "config.json" dans le dossier « config ».

{

  "ca_file": "/etc/tls/ca.crt",

  "cert_file": "/etc/tls/consul.crt",

  "key_file": "/etc/tls/consul-key.key",

  "verify_incoming": true,

  "verify_outgoing": true,

  "verify_server_hostname": true,

  "ports": {

    "https": 8443

  }

}

Le fichier reprend le nom des certificats créé précédemment.

Je stocke cette configuration dans un objet "configmap" de Kubernenets

 

kubectl create configmap cfm-consul-traefik-lan --from-file=config/config.json --namespace=prd-lan-coolcorp

Tout est prêt pour lancer le cluster Consul.

Création de l'objet service pour Consul

Je traite d’abord l’objet service dans un fichier "01-svc-consul-for-traefik.yaml" qui va être associé à mon cluster Consul et qui va permettre de rediriger les requêtes vers l’un ou l’autre des pods contenant un conteneur consul.

apiVersion: v1

kind: Service

metadata:

  namespace: prd-lan-coolcorp

  name: svc-consul-for-traefik

  labels:

    app: consul-for-traefik

    zone: lan

    env : prd

spec:

  #clusterIP: None

  ports:

    - name: http

      port: 8500

      targetPort: 8500

    - name: https

      port: 8443

      targetPort: 8443

    - name: rpc

      port: 8400

      targetPort: 8400

    - name: serflan-tcp

      protocol: "TCP"

      port: 8301

      targetPort: 8301

    - name: serflan-udp

      protocol: "UDP"

      port: 8301

      targetPort: 8301

    - name: serfwan-tcp

      protocol: "TCP"

      port: 8302

      targetPort: 8302

    - name: serfwan-udp

      protocol: "UDP"

      port: 8302

      targetPort: 8302

    - name: server

      port: 8300

      targetPort: 8300

    - name: consuldns

      port: 8600

      targetPort: 8600

  selector:

    app: consul-for-traefik

Le point important est la partie "selector". Pour que mon service soit associé à mon déploiement, il faudra que je reprenne le même label "app: consul-for-traefik".

kubectl apply -f 01-svc-consul-for-traefik.yaml

Création du Deployment pour consul

Je traite maintenant le déploiement de Consul via le fichier yaml suivant "02-stf-consul-for-traefik.yaml":

apiVersion: apps/v1

kind: StatefulSet

metadata:

  name: stf-consul-for-traefik

  namespace: prd-lan-coolcorp

spec:

  serviceName: svc-consul-for-traefik

  replicas: 3

  selector:

    matchLabels:

      app: consul-for-traefik

  template:

    metadata:

      labels:

        app: consul-for-traefik

    spec:

      securityContext:

        fsGroup: 1000

      containers:

        - name: consul-for-traefik

          image: "consul:1.7.2"

          env:

            - name: POD_IP

              valueFrom:

                fieldRef:

                  fieldPath: status.podIP

            - name: GOSSIP_ENCRYPTION_KEY

              valueFrom:

                secretKeyRef:

                  name: sec-consul-traefik-lan

                  key: gossip-encryption-key

            - name: NAMESPACE

              valueFrom:

                fieldRef:

                  fieldPath: metadata.namespace

          args:

            - "agent"

            - "-advertise=$(POD_IP)"

            - "-bind=0.0.0.0"

            - "-bootstrap-expect=3"

            - "-retry-join=stf-consul-for-traefik-0.svc-consul-for-traefik.$(NAMESPACE).svc.cluster.local"

            - "-retry-join=stf-consul-for-traefik-1.svc-consul-for-traefik.$(NAMESPACE).svc.cluster.local"

            - "-retry-join=stf-consul-for-traefik-2.svc-consul-for-traefik.$(NAMESPACE).svc.cluster.local"

            - "-client=0.0.0.0"

            - "-config-file=/consul/myconfig/config.json"

            - "-datacenter=lan"

            - "-data-dir=/consul/data"

            - "-domain=cluster.local"

            - "-encrypt=$(GOSSIP_ENCRYPTION_KEY)"

            - "-server"

            - "-ui"

            - "-disable-host-node-id"

          volumeMounts:

            - name: config

              mountPath: /consul/myconfig

            - name: tls

              mountPath: /etc/tls

          lifecycle:

            preStop:

              exec:

Plusieurs éléments de configuration sont importants. L’option en rouge datacenter doit correspondre au mot clef que j’ai utilisé pour la génération du certificat : lan

J’utilise bien le label app: consul-for-traefik pour qu’il "match" avec mon service créé juste avant.

Enfin il s’agit de faire appel à un des objets de type "StatefulSet" avec un replica de 3 pods.

Je vais reprendre la définition officielle:

"Un objet StatefulSet représente un ensemble de pods dotés d'identités uniques persistantes et de noms d'hôtes stables que kubernetes conserve, quel que soit l'endroit où ils sont planifiés. Les informations d'état et autres données résilientes relatives à un pod d'un objet StatefulSet donné sont conservées dans un stockage sur disque persistant associé à l'objet StatefulSet."

À l’origine Kubernetes était surtout fait pour traiter des applications « sans état » dont l’arrêt de son(ses) conteneur(s) sur un nœud et le redémarrage sur un autre avec une identité différente (mais un rôle identique) ne pose aucun souci. Grâce aux objets « StatefulSet », Kubernetes peut aussi tenir compte des applications « avec états » ou il est primordial qu’un pod A avec un nom MyNameA et qui a généré des données DataA soit toujours associé à son nom MyNameA et ses DataA.

C’est le cas pour Consul, puisque chaque « intance » qui prend la forme d’un pod devra toujours retrouver ses billes en cas de soucis. Cela a un impact sur la persistance de la donnée. Dans le yaml, il n’y’a pas référence à un stockage spécifique, ce dernier se fera donc sur le nœud qui exécute le pod, créant une dépendance nœud/pod…ce qui n’est normalement jamais le but recherché. La solution serait d’utiliser un objet « PersistentVolumeClaim » qui utiliserait, par exemple un datastore ou un partage NFS accessible par tous les nœuds kubernetes. Ainsi si mon "node1" tombe et qu’il faisait tourner un pod Consul, ce dernier peut redémarrer sur un "node2" et retrouver ses données et son nom. Etant donné que l’on déploie Consul en cluster ce n’est pas nécessaire. Si un pod venait à tomber, il peut être redémarré avec des données vierges, lorsqu’il va rejoindre le cluster, il pourra récupérer une copie de la base, puisque chaque membre du cluster dispose de la donnée répliquée. Cela nécessite bien entendu d’avoir au moins toujours 2 pods Consul qui tournent. Donc en résumé, pour avoir une bonne sécurité, sans utiliser de stockage partagé, il faudrait s’assurer qu’au moins trois pods Consul d’un même cluster tournent sur trois nœuds kubernetes différents. Ce qui n’est pas le cas dans ce lab…mais mon infra "homemade" à ses limites.

 

Ces remarques mettent en avant l'importance de bien penser son architecture en amont de son projet. Il faut définir son besoin de redondance et de disponibilité pour connaitre les prérequis en termes de pods et de noeuds K8S. Il faut également concevoir ses fichiers de déploiement en y intégrant ces contraintes pour indiquer à Kubernetes comment répartir les pods. Kubernetes orchestre l'exécution des conteneurs, mais ne réfléchis par pour vous, il se "contente" d'appliquer les règles que vous lui donner.

 

Je lance donc le déploiement de consul.

 

kubectl apply -f 02-stf-consul-for-traefik.yaml

Je vérifie que les pods associés ont démarré.

kubectl get pods --namespace=prd-lan-coolcorp

pod consul

Notez que le nom des pods n'utilise pas comme "NAME" un titre associé à une référence aléatoire. On n'a une énumération logique correspondant au numéro d'instance Consul. Cela est lié à l'usage des StatefulSet

Si les pod sont démarrés, je veux m’assurer que tout est OK en interrogeant les logs d’au moins l'un d'entre eux.

kubectl logs stf-consul-for-traefik-0 --namespace=prd-lan-coolcorp

Ce qui m’intéresse, c’est la partie concernant l’élection d’un leader au sein du cluster. Si cette étape est OK, alors mon cluster Consul est up.

Vérifiation des logs consul

Il me reste maintenant à accéder à l’interface graphique. Pour l’instant, cette dernière n’est pas accessible en dehors du contexte du cluster kubernetes.

Accès à l'interface consul 

Je vais donc utiliser la commande suivante sur mon poste de travail

kubectl port-forward stf-consul-for-traefik-0 8500:8500 --namespace=prd-lan-coolcorp

Cette dernière va me permettre d’encapsuler les requêtes de mon poste de travail à destination de l’ip local sur le port 8500 vers le port 8500 du premier pod Consul.

Redirection des ports

J’accède bien à l’interface et si je vais sur le menu node, j’ai bien trois instances.

 

GUI consul

 

En production, il resterait au moins deux taches supplémentaires

 

- Des tests de pertes d’instance pour évaluer la haute disponibilité de la solution

- Des tests de sauvegardes/restaurations pour s’assurer de pouvoir récupérer le contenu de la base clef/valeur en cas de problèmes.

 

J’espère pouvoir intégrer ces éléments dans une étape dédiée une fois toute la solution déployée.