Intégration finale
Introduction
Nous disposons à présent de l’ensemble des moyens techniques et fonctionnels permettant d’atteindre l’objectif final : rendre l’ensemble de nos services totalement redondants et tolérants aux pannes.
Il reste à supprimer les derniers points de défaillance uniques (SPOF). Les moyens déjà mis en place ainsi que ceux proposés par Kubernetes vont nous permettre de franchir les derniers obstacles et de conclure notre fil rouge.
Volumes persistants
1. Cluster NFS-HA
Nous avons déjà abordé les volumes persistants dans le chapitre Déploiement avec Kubernetes. Par ailleurs, dans le chapitre Stockage en haute disponibilité, nous avons créé un cluster NFS en haute disponibilité, avec un playbook Ansible permettant de créer des partages à volonté. Kubernetes supporte les volumes de type NFS. Notre exemple se base sur des VM VirtualBox et nous ne disposons pas de classes de stockage (storage class) pour ces volumes. Mais nous pouvons adapter notre playbook pour créer des volumes persistants à l’aide d’un template, et l’ajouter à Kubernetes.
Le template Jinja nfs-pv.yaml.j2 est le suivant :
apiVersion: v1
kind: PersistentVolume
metadata:
name: {{ final_pvname }}
labels:
size: {{ pvsize }}i
format: xfs
{% if labels is defined %}{% for k,v in labels.items() %}
{{ k }}: {{ v }}
{% endfor %}{% endif %}
spec:
capacity:
storage: {{ pvsize }}i
accessModes:
- ReadWriteOnce
- ReadWriteMany
nfs:
path: /{{ final_pvname }}
server: {{ nfs_vip }}
persistentVolumeReclaimPolicy: Recycle
{% if storageclass is defined %} storageClassName:
{{ storageclass }}{% endif %}
NFS permet d’utiliser le mode d’accès RWX (Read-Write-Many) : le volume peut être partagé entre plusieurs pods, ce qui est idéal pour notre application eni-todo et son téléchargement de fichiers. Mais on peut aussi utiliser le mode RWO (Read-Write-Once) : un volume n’est associé qu’à un seul pod et ne peut pas être partagé.
La ligne Recycle indique comment...
Registry Docker
1. Architecture
Dans le chapitre Infrastructure et services de base, nous avons installé notre registry Docker sur le serveur infra01. Ce service n’est pas en haute disponibilité. Sa perte n’est pas forcément grave (il se réinstalle vite) ; il faut soit attendre son redémarrage, soit, au pire, en cas de perte du stockage, reconstruire les images (si l’on n’a pas de sauvegarde). Cela reste cependant un SPOF. Nous allons utiliser tous les moyens à notre disposition pour résoudre ce problème.
Nous allons déployer un ou plusieurs pods de registry, issus de l’image fournie par Docker, la même que dans le chapitre Infrastructure et services de base. Le registry que nous allons créer va utiliser un des volumes persistants que nous venons de créer pour y stocker nos images.
Le registry sera exposé à l’aide d’un service et surtout d’un routeur ingress. Nos répartiteurs de charge HAProxy proposeront un accès global en se plaçant en amont de l’ingress.
On pourrait déployer ce registry en HTTP jusqu’aux routeurs ingress et gérer la terminaison SSL/TLS uniquement sur les répartiteurs HAProxy. Mais c’est l’occasion d’appliquer certaines techniques du contrôleur ingress Nginx : nous allons communiquer de manière chiffrée de bout en bout.
Figure 1 : Intégration d’un registry Docker dans Kubernetes
2. Génération d’un secret
Créez tout d’abord le namespace.
$ kubectl create ns registry
Les pods doivent pouvoir accéder à des clés et à des certificats, ceux de registry.diehard.net.
Créez un secret de type TLS (voir le chapitre Déploiement avec Kubernetes), que vous pourrez utiliser aussi bien pour le registry que pour la route ingress, en précisant son nom et où trouver les valeurs (adaptez le chemin en conséquence).
$ kubectl -n registry create secret tls ssl-key-cert \
--key=./certs/registry.diehard.net.key \
--cert=./certs/registry.diehard.net.crt
secret/ssl-key-cert created
Le résultat est sous la forme suivante (tronquée). Les valeurs sont encodées (mais pas chiffrées) au format base64 :
$ kubectl...
Base de données
1. Problématique
Jusqu’à présent, nous avons utilisé une base de données dite standalone (autonome) : une seule instance démarrée sur un serveur (modèle on-premise) ou démarrée sous forme de container. Le problème est évident : il n’y a aucune tolérance aux pannes. En cas d’arrêt volontaire ou non du service, notre application eni-todo n’est plus accessible. C’est donc un SPOF que nous allons éliminer.
Plusieurs solutions permettent d’assurer une disponibilité, avec ou sans Kubernetes. Parmi celles basées sur MySQL ou MariaDB en mode cluster, on distingue les solutions multimaîtres, où tous les serveurs sont actifs en lecture et en écriture et se synchronisent entre eux, et les solutions de type master-réplicas, où un maître accepte les écritures, et les replicas les lectures, avec une répartition des enregistrements (les shards).
On trouve par exemple les solutions Galera (multimaîtres) ou Vitess (master-réplicas).
Dans le chapitre Déploiement avec Kubernetes, nous avons présenté une ébauche de solution basée sur les StatefulSets, avec une instance MariaDB maître et un ou plusieurs réplicas. Comme il n’y a qu’un seul maître et que les réplicas sont en lecture, si on perd le maître, alors des manipulations sont nécessaires pour basculer vers un nouveau maître, tant du côté du cluster que du côté des applications déployées. Si on peut réduire les modifications manuelles, c’est idéal.
Nous pourrions aussi envisager d’utiliser un cluster de type Pacemaker, comme pour notre cluster NFS-HA, mais en faisant basculer un service MySQL, son stockage et son IP. Ce qui signifie aussi que cette solution a besoin d’un stockage persistant en haute disponibilité. On pourrait alors utiliser un export présenté par le premier cluster ou même ajouter des nouvelles ressources sur le cluster Pacemaker existant.
Une autre solution tout à fait envisageable, déjà mise en place par les auteurs sur des projets peu critiques, consiste à déployer un unique pod MySQL ou MariaDB via Kubernetes...
eni-todo
1. Adaptations
Nous disposons maintenant de tous les éléments pour déployer notre application : volumes, base de données, routeurs, répartiteurs de charge, etc. Nous avons déjà déployé l’application eni-todo dans le chapitre Déploiement avec Kubernetes, avec l’environnement de test sous Minikube. Quelques petits changements s’appliquent néanmoins. C’est dans ce genre de situation que les principes du DevOps montrent leur intérêt.
Tout d’abord, les enregistrements de type session, qui dépendaient fortement de la persistance des connexions, peuvent maintenant être nativement gérés par une symbiose d’Apache Tomcat et de Kubernetes. Apache Tomcat possède des mécanismes de réplication de session depuis longtemps, notamment à travers le module Tribe. Apparue récemment, la classe org.apache.catalina.tribes.membership.cloud.KubernetesMembershipProvider permet d’interroger l’API de Kubernetes, permettant à Tomcat de découvrir par lui-même ses pairs et de configurer le partage des sessions. Ainsi, les enregistrements sont préservés, sauf si on coupe tout, bien entendu. L’intérêt n’est pas uniquement lié aux informations de sessions gérées par eni-toto : l’application pourrait évoluer en incluant par exemple un gestionnaire de connexions et de droits, dont les informations seraient partagées entre les instances, n’obligeant pas l’utilisateur à se reconnecter (sans parler des pertes de données) en cas de crash ou de redémarrage d’une d’entre elles. Bien entendu, il faut garder en tête le coût réseau de ces échanges et la taille des sessions répliquées.
Nous en profiterons pour ajouter la notion de persistance de bout en bout : sauf perte de l’instance courante, nous ferons en sorte que la connexion se fasse toujours sur le même pod.
Ensuite, nous allons utiliser le cluster Galera créé précédemment, lui-même redondant, sans aucune autre adaptation que celle d’utiliser le nom DNS interne du service Kubernetes.
Dans cet exemple nous plaçons le cluster et l’application dans deux namespaces différents....
Bilan
Que de chemin parcouru depuis le premier chapitre et l’installation standalone de notre application ! De l’infrastructure jusqu’à l’application en passant par les accès, tout est maintenant redondant et tolérant aux pannes. Nous avons atteint l’objectif : notre application est en haute disponibilité.
Au-delà des mécanismes techniques mis en place, l’atteinte de cet objectif est le résultat d’une proche collaboration entre le développeur de l’application eni-todo et l’opérateur chargé de la plateforme, autrement dit entre le Dev et l’Ops. Le développeur doit prendre en compte les remarques de l’Ops sur l’architecture de son produit : intégrer un mécanisme de session, penser que plusieurs instances applicatives devront tourner, permettre à l’application de tourner dans un container… L’Ops doit prendre en compte les besoins du développeur : correctement dimensionner les ressources, gérer des mécanismes d’affinité, proposer le stockage nécessaire, etc.
Et on voit à quel point la frontière semble parfois devenir floue sur certains aspects ! Kubernetes, par exemple, nécessite de profondes connaissances système et réseau pour son installation et sa maintenance, mais aussi de grandes...