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
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.
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
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 :
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
À l’aide du binaire consul, je génère une clef de chiffrement avec la commande "consul keygen"
Je vais temporairement utiliser cette clef comme variable d’environnement depuis la console qui va me service à générer mon secret dans Kubernetes.
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.
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
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
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.
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.
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.
J’accède bien à l’interface et si je vais sur le menu node, j’ai bien trois instances.
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.