Aller plus loin
Introduction
Bien que notre objectif soit atteint, nous souhaitons vous donner le plus d’informations possible sur les étapes suivantes, tant organisationnelles que techniques, et les possibilités d’évolution qui s’offrent pour améliorer le fonctionnement de notre plateforme, la faire évoluer et assurer une meilleure sécurité.
Plan de reprise d’activité
1. Quand le désastre arrive
Figure 1 : Quand le pire arrive
Quand on parle de haute disponibilité, on parle souvent, en parallèle, de reprise d’activité, d’où l’expression HA-DR (High Availability-Disaster Recovery).
Si le but de la haute disponibilité est de garantir la disponibilité des applications dans le plus grand nombre de cas de figure possible, il faut aussi prévoir ce qui semble parfois improbable : une panne majeure qui conduit à l’indisponibilité partielle ou totale du service, voire à sa destruction totale.
C’est souvent une leçon apprise trop tard : il ne faut jamais pécher par excès de confiance. Rappelons la loi de Murphy : tout ce qui est susceptible d’aller mal ira mal. Et le corollaire de Finagle (qui est un personnage de fiction) : tout ce qui peut aller mal arrivera au pire moment. Rappelons aussi qu’il existe deux sortes d’ingénieurs système : ceux qui ont fait une énorme bêtise et ceux qui vont la faire.
Les auteurs ont eu l’occasion d’expérimenter en production réelle divers cas de figure. Les métiers liés à la production informatique mettent parfois les nerfs à rude épreuve. Comment réagir lorsqu’on perd l’interconnexion entre deux centres de données ? Comment réagir lorsque quelqu’un éteint tous les répartiteurs de charge en même temps, coupant ainsi intégralement les accès à l’ensemble des serveurs ?
Tout système créé par un être humain est forcément imparfait. La plupart des incidents et accidents sont d’ailleurs le fait d’interventions humaines, involontaires ou non.
Quand tout va mal, que tout est perdu, il faut alors penser au plan B, voire au plan C. Comment redémarrer ? En ayant anticipé la catastrophe et mis en place un plan de redémarrage des services.
L’incendie d’un centre de données d’OVH en mars 2021 est un rappel de l’importance du plan de reprise d’activité et une leçon durement apprise par ceux qui croyaient à la magie du cloud, qui pensaient qu’un développeur fullstack pouvait remplacer un Ops ou un architecte, que la redondance rendait inutiles les sauvegardes, qu’on pouvait baser l’intégralité de son activité commerciale sur une simple offre d’hébergement à quelques euros par mois…
2. Définition
Un plan de reprise d’activité ou PRA (Disaster Recovery Plan ou DRP) est un ensemble de procédures décrites à l’avance permettant de reconstruire un système d’information et de rétablir les services en cas de sinistre.
Un sinistre est un événement impondérable et imprévu : catastrophe naturelle (tremblement de terre, inondation, tempête, tsunami, ouragan, canicule…), vol, attentat, sabotage, incendie, dégât des eaux… mais aussi acte de malveillance (dégradation volontaire, piratage, demande de rançon, effacement des données, virus, malware). Il faut aussi penser à l’erreur humaine dont les conséquences peuvent être désastreuses (effacement de données, extinction de serveurs…).
Il n’y a pas une semaine sans qu’une compagnie, un hôpital, une mairie ou un service public, perdent toutes leurs données à la suite d’une attaque informatique.
Tous ces événements doivent être répertoriés et anticipés.
HA et DR peuvent-ils fonctionner de concert ? Oui, si l’architecture technique, fonctionnelle et la topologie des datacenters et des applications le permettent. En pratique cependant, même des déploiements multizones en actif-actif présentent parfois des points faibles. Il faut souvent mettre en place une reprise manuelle en cas d’incident.
3. Mesures
Les mesures de base d’un PRA sont la prévention, la détection et la correction. Tout ceci s’inscrit dans le cadre global de l’anticipation des événements : il faut être proactif.
-
Toutes les données (y compris les codes, logiciels, artefacts de construction, etc.) doivent être sauvegardées, et faire l’objet de tests de restauration.
-
Les sauvegardes doivent être externalisées.
-
Toutes les actions (les playbooks, par exemple) doivent être documentées.
-
Tout plan doit être testé régulièrement, au moins une fois par an.
-
Les services et les serveurs doivent être inspectés : monitoring (sondes) bien entendu, mais aussi, au sein des centres de données, inspection visuelle (voyants sur les serveurs, par exemple).
-
La sécurité doit être optimale : système, réseau, services, mais aussi locaux (intrusion physique, incendie, inondation, etc.).
-
Les serveurs doivent être mis à jour régulièrement et leur paramétrage doit suivre les recommandations des éditeurs et des organismes de sécurité.
-
On ne met pas « tous ses œufs dans le même panier » : y compris dans le cloud, on doit pouvoir démarrer sur d’autres zones, régions, voire avec d’autres fournisseurs.
-
La réversibilité, c’est-à-dire pouvoir revenir à une solution initiale, doit être envisagée. Par exemple, pouvoir sortir du cloud et revenir dans un datacenter. Des tests d’intrusion (pentest, red team) doivent être effectués régulièrement.
-
Les employés doivent être formés.
-
Le PRA et les délais de remise en service doivent être contractualisés.
-
Un mode dit « dégradé » peut permettre un redémarrage incomplet.
-
L’organisation doit être éprouvée : chacun sait qui déclenche le PRA, chacun sait ce qu’il a à faire.
-
Un responsable ou coordinateur doit être nommé : garant de l’exécution ordonnée du plan et ayant autorité...
Sauvegardes
1. Redondance vs sauvegarde
Un RAID, un cluster, toute solution redondante ne sont pas des sauvegardes. Une copie locale archivée sur un disque dur, du même serveur ou d’un serveur dans la même zone, n’est pas une sauvegarde. Ne confiez pas vos données à un informaticien vous disant le contraire.
Il est très important de souligner cette évidence. Les événements de mars 2021 qui ont conduit à la perte définitive de milliers de sites web et de services, voire à la perte de l’ensemble des données de certaines entreprises, sont un nouveau rappel à l’ordre.
Figure 3 : Pas de sauvegarde... Le désespoir à la suite de la perte d’un centre de données
Que ce soit sur le cloud ou dans un datacenter qui vous appartient, les sauvegardes sont nécessaires et vitales. Dans un monde devenu numérique, la donnée est le composant vital de toute organisation, mais pas seulement. Combien de drames sont liés à la perte des photos de famille à la suite du crash d’une application, à la défaillance d’une clé USB, d’une carte mémoire, d’un disque dur ?
Les sauvegardes doivent être externalisées : elles ne doivent pas se situer sur le même centre de données que les serveurs. Si possible, elles doivent être disponibles en double exemplaire. Sur un cloud, on peut utiliser des volumes particuliers dupliqués entre plusieurs zones et régions pour garantir leur disponibilité. Sous AWS par exemple, des buckets S3 peuvent être répliqués entre plusieurs régions, et même entre buckets.
Un compartiment (bucket) s3 est un mode de stockage objet proposé par le fournisseur de cloud Amazon. Ce stockage est accessible via le protocole HTTP.
La même chose existe bien sûr sur Azure, où les données et les sauvegardes peuvent être répliquées selon plusieurs modèles, au sein d’une même région, entre plusieurs zones, et entre plusieurs régions. Les sauvegardes doivent être testées : on s’assure régulièrement qu’elles sont accessibles et disponibles, que les données peuvent être restaurées, notamment en cas de montée de version. Il arrive qu’une version n+1 ne puisse plus relire les anciennes données. On s’assurera alors de pouvoir réinstaller une version du logiciel pouvant relire ces données. On met en place un plan de sauvegarde qui définit :
-
quoi sauvegarder ;
-
quand sauvegarder ;
-
où sauvegarder ;
-
combien de temps conserver les sauvegardes.
2. Que sauvegarder ?
Tout sauvegarder n’a pas toujours du sens. Si on est capable de restaurer intégralement des serveurs via des processus d’automatisation, parce que ces serveurs sont normalisés, alors il est plus pertinent de faire une sauvegarde de ces processus que des serveurs eux-mêmes. Sur le cloud ou pour des machines virtuelles, on peut cependant s’appuyer sur des mécanismes de snapshots pour sauvegarder une machine dans un état donné.
-
On sauvegardera tous les playbooks et scripts permettant de reconstruire la plateforme.
Doit-on sauver les images applicatives, ou leur code, leur Dockerfile et les scripts de build ? Si on est capable de reconstruire rapidement les images, alors pourquoi pas... Mais si on parle de centaines d’images à reconstruire, peut-être est-il pertinent de sauver les artefacts complets.
Dans notre cas, par exemple, il y a plusieurs dépendances externes des images Docker. D’abord l’image de base (tomcat:10.1-jre21-temurin), puis les éléments de build (maven:3.9-eclipse-temurin-21), et enfin les JAR du projet.
-
On sauvegardera a minima l’ensemble du code, ses dépendances et le nécessaire pour reconstruire les applications.
-
On sauvegardera les images Docker elles-mêmes si on veut repartir au plus vite.
Pour les données (bases de données, fichiers dépendants, assets statiques, par exemple les fichiers téléchargés de eni-todo), la question ne se pose même pas : il faut sauvegarder.
-
On sauvegardera l’ensemble des données et leurs dépendances.
Quant à Kubernetes, il existe plusieurs stratégies de sauvegarde, qui incluent d’ailleurs les précédentes. Si on dispose de tous les fichiers YAML pour recréer nos projets, c’est très bien. Mais il ne faut pas oublier que la configuration de Kubernetes est intégralement placée dans une base etcd.
-
On sauvegardera la base etcd.
Le protocole de sauvegarde de la base etcd est accessible depuis l’adresse https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/.
Kubernetes a des dépendances extérieures. Si les volumes persistants et les demandes de volumes persistants sont bien des objets Kubernetes, les données qui y sont stockées ne le sont pas.
On sauvegardera les volumes persistants.
3. GitOps
Le GitOps est une pratique qui consiste à stocker la configuration de ses composants d’infrastructure dans un dépôt...
Sécurité
1. Container et root
a. Exemple d’exploit
Depuis le début, et mis à part le déploiement du cluster Galera, nous avons déployé l’application eni-todo sans nous soucier de ses droits, aussi bien lors de la construction de l’image que lors de son exécution. Or, notre application tourne avec l’UID 0, en tant que root. Nous avons également désactivé apparmor pour des raisons de simplification.
La plupart des images disponibles publiquement, que ce soit sur Docker Hub ou dans des dépôts Git, ne tiennent pas compte de la gestion des utilisateurs : au sein du container, tout ou presque tourne en tant que root.
Or, on applique normalement le principe du moindre privilège : on ne devrait faire tourner un produit qu’avec les droits qui lui sont strictement nécessaires.
Prenons l’exemple de la simple image Nginx de test utilisée dans le chapitre Mise en place d’un cluster Kubernetes, et voyons ce qu’il est possible de faire avec.
$ docker run --name some-nginx -d nginx
...
11a90c92c2c3b3507fa5068198a780bf9be39a74df5f427f8a601945c9d15a54
Si vous ouvrez un shell interactif au sein de ce container, vous voyez que vous êtes root.
$ docker exec -ti 11a90c92c2c3 bash
root@11a90c92c2c3:/# id
uid=0(root) gid=0(root) groups=0(root)
Cela ne signifie pas forcément que nous pouvons aussi obtenir les droits root sur l’hôte : le container s’exécute dans son propre namespace. Notre container Nginx est ici limité et le risque est faible. Mais par défaut, les containers et l’hôte se partagent UID et GID. Si, par une faille de sécurité ou via un démarrage de container construit d’une certaine manière, le container a accès aux composants de l’hôte, alors cela devient dangereux. Voici un exemple simple avec les volumes en montant le répertoire /etc de l’hôte au sein du container :
seb@infra01:~$ docker run --name some-nginx -v /etc:/mnt/etc -d
9f4d178c388e3df5e8e0f68137054339e7b0a0fbd8562245ddf4263fd7685cbd
seb@infra01:~$ docker exec -ti 9f4d178c388e3 bash
root@9f4d178c388e:/# echo "seb ALL=(ALL) NOPASSWD: ALL" >
/mnt/etc/sudoers.d/seb
root@9f4d178c388e:/# exit
exit
seb@infra01:~$ sudo -l
User seb may run the following commands on infra01:
(ALL) NOPASSWD: ALL
L’utilisateur seb, sans pouvoirs particuliers, a lancé un container qui lui a permis de modifier ses droits sur l’hôte. Il ne s’agit pas d’une faille de sécurité, mais du fonctionnement normal. Ceci est possible parce que le service Docker s’exécute en tant que root et que le démarrage du container a été construit d’une manière le permettant.
Dans ce cas, un éventuel hacker n’a pas à se soucier de trouver une faille sur le compte root directement, il pourra pivoter depuis le compte seb, via Docker, pour s’accaparer tous les droits sur la machine hôte.
b. Solutions
Il est possible d’éviter assez simplement ce comportement via plusieurs mécanismes. Le premier est de créer des images et des containers qui n’utilisent pas root. L’instruction USER au sein du Dockerfile permet d’exécuter des commandes en changeant de contexte utilisateur.
RUN useradd -ms /bin/bash my_user
USER my_user
Tout ce qui est situé après la commande USER s’exécute sous cet utilisateur. Il faut alors faire en sorte que cet utilisateur puisse démarrer l’application, accéder à ses fichiers de configuration, et lire et écrire dans ses volumes, ce qui demande une certaine rigueur.
Vous pouvez définir l’utilisateur et le groupe d’exécution au moment de lancer le container, ici avec l’UID 1001 et le GID 1001. Encore une fois, l’image doit avoir été construite en conséquence.
$ docker run --user 1001:1001 ...
Ensuite, il est possible d’utiliser les mécanismes du user namespace : vous pouvez dissocier les UID et GID du container de ceux de l’hôte. Les deux fichiers /etc/subuid et /etc/subgid définissent les règles.
$ cat /etc/subuid
seb:100000:65536
Si vous utilisez un mapping avec l’utilisateur seb, l’UID 0 du namespace est associé à l’UID 100000 de l’hôte, et tous les processus lancés au sein du container appartiennent aux UID compris entre 100000 et 165535. Par exemple, l’image Nginx définit un utilisateur d’UID 101. Avec ce système, il est vu comme UID 100000+101 depuis l’hôte, tout en étant vu comme l’UID 101 depuis le container.
Le test est simple. Modifiez tout d’abord le fichier /etc/docker/daemon.json pour activer le mapping.
{
"userns-remap": "seb"
}
Puis redémarrez Docker.
$ sudo systemctl restart docker
Enfin, testez à nouveau avec l’image Nginx. Vous voyez bien que, les utilisateurs étant root au sein du container, les UID sont différents, vus de l’hôte :
$ docker run --name some-nginx2 -d nginx
$ docker exec -ti 79f9b398aa94 bash
root@79f9b398aa94:/# id
uid=0(root) gid=0(root) groups=0(root)
root@79f9b398aa94:/# exit
seb@infra01:~$ ps ax -o cmd,uid | grep nginx
nginx: master process nginx 100000
nginx: worker process 100101
Vous pouvez aussi décider de ne pas utiliser Docker et de le remplacer par un autre outil de gestion de containers, comme Podman (https://podman.io/). Podman lance ses containers en espace utilisateur, évitant ainsi les problèmes de sécurité liés au compte root. Les auteurs ont longuement comparé Docker et Podman, chacun ayant ses qualités et ses défauts. Podman représente un avantage en matière de sécurité, mais cela entraîne des baisses de performances, notamment sur la vitesse du réseau.
c. Le contexte de sécurité (securityContext)
Dans Kubernetes, il est possible de forcer l’id du groupe ou de l’utilisateur avec un contexte de sécurité via un objet securityContext.
apiVersion: v1
kind: Pod
metadata:
name: secure-eni-todo
spec:
securityContext:
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
Nous avons abordé ce concept lorsqu’il a fallu adapter notre cluster Galera aux droits des volumes persistants NFS, en forçant les pods à démarrer avec l’UID utilisateur 10000. Il est possible également de configurer SELinux ou AppArmor pour ajouter encore un niveau de sécurisation aux pods.
2. Réseau Kubernetes
Kubernetes s’appuie sur un modèle réseau flexible permettant de disposer de ses propres modèles et contrôleurs. Nous avons utilisé Flannel parce qu’il est simple et efficace pour illustrer tous les exemples de ce livre. Mais Flannel n’offre aucune protection spécifique, aucune segmentation entre les pods, les services, les namespaces. Nous avons pu illustrer très simplement ce fait en autorisant eni-todo à se connecter au service de base de données Galera qui fait pourtant partie d’un autre namespace : Flannel ne propose aucune isolation réseau.
Si ce n’est pas un problème dans le cadre d’un cluster Kubernetes entièrement dédié à notre application, ça l’est si le cluster a pour vocation d’être multienvironnement (développement, intégration, production...) et multiclient, avec des projets bien distincts devant être isolés.
Kubertenes dispose...
Surveillance
1. Métriques
Les métriques sont essentiellement la collecte d’informations sur les processeurs, la mémoire, les disques et le réseau afin d’en vérifier les performances. Grâce à ces valeurs et à leur suivi, il devient possible d’analyser les consommations des différentes ressources et d’agir en conséquence :
-
créer de nouveaux serveurs ;
-
étendre l’espace disque ;
-
ajouter de la mémoire ;
-
mieux répartir les services.
La surveillance des métriques se fait souvent avec des outils de monitoring et des sondes comme Nagios et Centreon, qui sont libres, et des protocoles comme SNMP. Des outils système en ligne de commande comme sar, iostat, mpstat, etc. permettent d’obtenir des informations utiles.
Kubernetes propose une API dédiée aux métriques, qui nécessite l’utilisation d’un contrôleur annexe et optionnel. Cette API donne accès à de nouvelles fonctions, comme l’autoscaling (ajout et suppression dynamique de pods en fonction d’une charge).
Pour tester cette fonctionnalité, il faut installer un pipeline de collecte de métriques. Il en existe de deux sortes selon qu’ils sont basés sur la collecte des métriques liées uniquement au cluster, par exemple metrics-server, ou sur la collecte des métriques liées à l’environnement complet, comme Prometheus.
Metrics-server est disponible ici : https://github.com/kubernetes-sigs/metrics-server
Prometheus, plus complet mais aussi plus complexe, est disponible ici : https://prometheus.io/
Vous pouvez installer rapidement metrics-server comme ceci :
$ kubectl apply -f https://github.com/kubernetes-sigs/
metrics-server/releases/latest/download/components.yaml
serviceaccount/metrics-server created
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-
reader created
clusterrole.rbac.authorization.k8s.io/system:metrics-server created
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:
auth-delegator created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created
service/metrics-server created
deployment.apps/metrics-server created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created
Modifiez juste un paramètre - sinon le pod ne démarre pas à cause des certificats par défaut - en éditant le déploiement et en ajoutant un argument :
$ kubectl -n kube-system edit deployment.apps/metrics-server
...
spec:
containers:
- args:
...
- --kubelet-insecure-tls
...
Dès lors, les métriques de base sont accessibles, par exemple avec kubectl top :
$ kubectl top node
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
k8s-cplane01 73m 3% 1670Mi 93%
k8s-cplane02 67m 3% 1422Mi 79%
k8s-cplane03 79m 3% 1481Mi 82%
k8s-worker01 40m 2% 1167Mi 65%
k8s-worker02 21m 1% 1247Mi 69%
k8s-worker03 18m 0% 1013Mi 56%
k8s-worker04 22m 1% 1142Mi 63%
Les métriques fonctionnent aussi pour les pods :
$ kubectl -n eni-todo top pod
NAME CPU(cores) MEMORY(bytes)
eni-todo-578d497b88-spcgr 1578m 228Mi
eni-todo-578d497b88-tn8dk 1472m 225Mi
Il est maintenant possible d’utiliser les deux modes d’autoscaling de Kubernetes :
-
Horizontal Pod Autoscaler (HPA) : ajustement automatique du nombre de réplicas en fonction de la charge mémoire ou CPU.
-
Vertical Pod Autoscaler (VPA) : ajustement automatique des ressources mémoire et CPU d’un container.
Nous pouvons rapidement tester une ressource HPA (horizontale) indiquant que eni-todo nécessite au minimum un pod, au maximum quatre...
Stockage
Au chapitre Déploiement avec Kubernetes, nous avons évoqué l’existence de plusieurs projets permettant la mise en place d’une infrastructure de stockage qui repose sur une infrastructure Kubernetes.
MinIO offre un ersatz de S3 dans un cluster Kubernetes.
Des projets comme OpenEBS ou GlusterFS déploient des pods avec des droits élevés pour monter les disques ou accéder au LVM sous-jacent afin d’utiliser ces nodes Kubernetes comme éléments du cluster de données. Il est alors possible d’ajouter des nodes pour répondre à la croissance des besoins de des applications.
Rook est également un projet d’infrastructure de stockage cloud-native ayant pour but de fournir les opérateurs Kubernetes permettant la mise en place de serveur Ceph, Cassandra ou NFS dans un cluster.
Bien que les opérateurs s’améliorent en permanence et fournissent des mécanismes de réparation, d’ajustement et de mise à jour de plus en plus automatisés, ces types d’outils restent extrêmement gourmands en ressources CPU, RAM, réseau et bien entendu en ressources liées aux accès en lecture et en écriture sur les disques hébergeant la solution.
Il est donc très important de bien cadrer les besoins et les objectifs avant de vouloir mettre un CephFS (Ceph File System) sur un cluster...
PaaS
Figure 5 : Système d’information classique vs solutions IaaS, PaaS ou SaaS
Kubernetes offre de très nombreux services et facilite grandement la difficile tâche de mettre en place une infrastructure haute disponibilité.
Les choix sont nombreux... Quel moteur de réseau utiliser, quel contrôleur ingress, quel moteur OCI (moteur de conteneurisation) ? Comment sécuriser son cluster et assurer le patch management de ses images ? Comment vérifier la cohérence de tous ces composants ?
Comme nous l’avons évoqué au chapitre Orchestration, c’est le rôle des distributions de gérer tout cela. La distribution que les auteurs ont le plus largement expérimentée est celle de Red Hat : OpenShift.
OpenShift est la première distribution à pousser une sécurité par défaut en interdisant notamment les pods en root par défaut, en imposant l’utilisation de SELinux et en fournissant un registry Docker sécurisé et segmenté.
OpenShift apporte également une intégration avec ses outils de monitoring et d’alerte orientés PaaS. La sécurité apportée par l’isolation des namespaces, dont la portée a été étendue aux projets (projects), est poussée à l’extrême directement dans ses composants d’infrastructure. Par exemple, un utilisateur ayant accès au projet A et pas au projet B n’a pas accès aux traces du projet B dans le service Kibana du cluster....