Vous êtes sur la page 1sur 282

Authorized French translation of material from the English edition

of Kubernetes : Up an d Running
ISBN : 978-2-49-193567-5
© 2017, Kelsey Hightower, Brendan Burns and Joe Beda
This translation is published and sold by permission of O’Reilly
Media Inc.,
which owns or controls all rights to publish and sell the same.

Illustration de couverture : Tryaging-iStock

© Dunod, 2019
11 rue Paul Bert, 92240 Malakoff
www.dunod.com

ISBN : 978-2-10-079254-2

Ce document numérique a été réalisé par PCA


Chez le même éditeur

Docker - Pratique des architectures à base de conteneurs, P-Y.


Cloux, T. Garlot, J. Kohler (2e édition) 336 pages, 2019.
Découvrir DevOps - L’essentiel pour tous les métiers,
S. Goudeau, S. Metias (2e édition) 240 pages, 2018.
Mettre en œuvre DevOps - Comment évoluer vers une DSI agile,
A. Sacquet, C. Rochefolle, (2e édition) 288 pages, 2018.
Scrum - Pour une pratique vivante de l’agilité, C. Aubry,
(5e édition) 384 pages, 2018.
Table

Couverture

Page de titre

Page de Copyright

Chez le même éditeur

Avant-propos

1 Introduction

1.1 Vitesse

1.2 Évolutivité de votre service et de vos équipes

1.3 Abstraction de votre infrastructure

1.4 Efficacité

2 Création et exécution de conteneurs

2.1 Images de conteneurs

2.2 Création d'images d'application avec Docker

2.3 Stockage d'images dans un registre distant

2.4 Le runtime de conteneur Docker

2.5 Nettoyage

3 Déploiement d'un cluster Kubernetes

3.1 Installation de Kubernetes sur un fournisseur de cloud public


3.2 Installation de Kubernetes en local à l'aide de minikube

3.3 Exécution de Kubernetes sur un Raspberry Pi

3.4 Le client Kubernetes

3.5 Composants du cluster

4 Commandes kubectl courantes

4.1 Espaces de noms

4.2 Contextes

4.3 Affichage d'objets API Kubernetes

4.4 Création, mise à jour et suppression d'objets Kubernetes

4.5 Étiquetage et annotation d'objets

4.6 Commandes de débogage

5 Pods

5.1 Pods dans Kubernetes

5.2 Penser en termes de pods

5.3 Le manifeste de Pod

5.4 Exécution des pods

5.5 Accès à votre Pod

5.6 Contrôles d'intégrité

5.7 Gestion des ressources

5.8 Persistance des données avec des volumes


5.9 Synthèse

6 Étiquettes et annotations

6.1 Étiquettes

6.2 Annotations

6.3 Nettoyage

7 Découverte des services

7.1 Qu'est-ce que la découverte des services ?

7.2 L'objet Service

7.3 Dépasser les limites du cluster

7.4 Intégration au cloud

7.5 Fonctionnalités avancées

7.6 Nettoyage

8 ReplicaSets

8.1 Boucles de rapprochement

8.2 Rapport entre les pods et les ReplicaSets

8.3 Conception avec des ReplicaSets

8.4 Spécifications des ReplicaSets

8.5 Création d'un ReplicaSet

8.6 Inspection d'un ReplicaSet

8.7 Mise à l'échelle des ReplicaSets


8.8 Suppression des ReplicaSets

9 DaemonSets

9.1 Ordonnanceur DaemonSet

9.2 Création des DaemonSets

9.3 Limitation des DaemonSets à des nœuds particuliers

9.4 Mise à jour d'un DaemonSet

9.5 Suppression d'un DaemonSet

10 Jobs

10.1 L'objet Job

10.2 Modèles de jobs

11 ConfigMaps et secrets

11.1 ConfigMaps

11.2 Secrets

11.3 Contraintes de nommage

11.4 Gestion des ConfigMaps et des secrets

12 Déploiements

12.1 Votre premier déploiement

12.2 Création de déploiements

12.3 Gestion des déploiements

12.4 Mise à jour des déploiements


12.5 Stratégies de déploiement

12.6 Suppression d'un déploiement

13 Intégration des solutions de stockage à Kubernetes

13.1 Importation de services externes

13.2 Exécution de singletons fiables

13.3 Stockage natif Kubernetes avec des StatefulSets

14 Déploiement d'applications réelles

14.1 Parse

14.2 Ghost

14.3 Redis

Annexe : Construction d'un cluster Kubernetes avec des


Raspberry Pi

Liste de courses

Génération des images

Premier démarrage : nœud master

Index
Avant-propos

◆ En forme de dédicace

Kubernetes souhaiterait remercier tous les administrateurs système


qui se sont réveillés à 3 heures du matin pour redémarrer un
processus, mais aussi tous les développeurs qui ont mis leur code
en production pour constater qu’il ne fonctionnait pas comme sur
leur ordinateur portable, et puis encore tous les architectes système
qui ont à tort lancé un test de charge sur un service de production en
raison d’un nom d’hôte inutilisé qu’ils n’avaient pas mis à jour. Ce
sont toutes ces souffrances, ces heures indues et ces erreurs
étranges qui ont inspiré le développement de Kubernetes. Si l’on
veut résumer notre propos en une seule phrase : Kubernetes a pour
objectif de simplifier radicalement les tâches de création, de
déploiement et de maintenance des systèmes distribués. Il a été
inspiré par des décennies de pratique de conception de systèmes
fiables et il a été élaboré à partir des usages de la base pour en faire
une expérience, si ce n’est euphorique, tout du moins agréable.
Nous espérons que vous allez apprécier ce livre !

◆ À qui s’adresse ce livre ?

Que vous soyez néophyte dans les systèmes distribués ou que vous
déployiez des systèmes cloud-native depuis des années, les
conteneurs et Kubernetes peuvent vous aider à réaliser des progrès
en matière de vitesse, d’agilité, de fiabilité et d’efficacité. Cet
ouvrage décrit l’orchestrateur de clusters Kubernetes et la façon
dont ses outils et son API peuvent être utilisés pour améliorer le
développement, la livraison et la maintenance des applications
distribuées. Bien qu’aucune expérience antérieure avec Kubernetes
ne soit requise, il est préférable d’être à l’aise avec la création et le
déploiement des applications basées sur un serveur si vous voulez
tirer le meilleur parti de ce livre. La familiarité avec des concepts
comme l’équilibrage de charge et le stockage réseau sera utile, mais
pas nécessaire. De la même manière, une expérience de Linux, des
conteneurs Linux, et de Docker, n’est pas essentielle, mais elle vous
aidera à profiter au maximum des ressources de cet ouvrage.

◆ Pourquoi avons-nous écrit ce livre ?

Nous avons été impliqués dans Kubernetes depuis son tout début.
Ce fut vraiment extraordinaire d’assister à la transformation de cette
technologie qui est passée d’une curiosité utilisée pour des
expériences à une infrastructure cruciale de production qui alimente
les applications à grande échelle dans des domaines variés qui vont
de l’apprentissage automatique (machine learning) aux services en
ligne. Quand cette transition s’est produite, il est devenu de plus en
plus clair qu’un livre qui décrirait à la fois la façon d’utiliser les
concepts de base de Kubernetes et les motivations sous-jacentes à
l’élaboration de ces concepts serait une contribution importante pour
le développement d’applications cloud-native. Nous espérons qu’à la
lecture de ce livre, non seulement vous allez apprendre à créer des
applications fiables et évolutives en vous appuyant sur Kubernetes,
mais que vous allez aussi comprendre les défis majeurs des
systèmes distribués qui ont conduit au développement de
Kubernetes.

◆ Un mot sur les applications cloud-native actuelles

Des premiers langages de programmation, à la programmation


orientée objet, en passant par le développement de la virtualisation
et des infrastructures dans le cloud, l’histoire de l’informatique est
une histoire du développement d’abstractions qui cachent la
complexité et vous permettent de créer des applications toujours
plus sophistiquées. Malgré cela, le développement d’applications
fiables et évolutives est encore beaucoup plus difficile qu’il ne devrait
l’être. Ces dernières années, les conteneurs et les API
d’orchestration de conteneurs comme Kubernetes sont devenus une
abstraction importante qui simplifie radicalement le développement
de systèmes distribués fiables et évolutifs. Bien que les conteneurs
et les orchestrateurs soient encore en phase de démocratisation, ils
permettent déjà aux développeurs de construire et de déployer des
applications avec une vitesse, une agilité et une fiabilité qui seraient
passées pour de la science-fiction il y a seulement quelques années.

◆ Organisation de ce livre

Ce livre est organisé de la manière suivante. Le premier chapitre


décrit les principaux avantages de Kubernetes sans rentrer trop
profondément dans les détails. Si vous n’avez jamais utilisé
Kubernetes, c’est l’occasion idéale pour commencer à comprendre
pourquoi vous devriez lire le reste du livre.
Le chapitre suivant fournit une introduction détaillée aux conteneurs
et au développement d’applications dans des conteneurs. Si vous
n’avez encore jamais vraiment testé Docker, ce chapitre constitue
une introduction utile. Si vous êtes déjà un expert Docker, ce sera
sans doute une révision.
Le chapitre 3 couvre la procédure de déploiement de Kubernetes.
Bien que la plus grande partie de ce livre se concentre sur la façon
d’utiliser Kubernetes, vous devez disposer d’un cluster opérationnel
avant de commencer à l’utiliser. Même si l’exécution d’un cluster
pour la production dépasse la portée de ce livre, ce chapitre
présente quelques méthodes simples pour créer un cluster afin que
vous puissiez comprendre comment utiliser Kubernetes.
À partir du chapitre 5, nous abordons en détail le déploiement d’une
application à l’aide de Kubernetes. Nous couvrons les pods
(chapitre 5), les étiquettes et les annotations (chapitre 6), les
services (chapitre 7), et les ReplicaSets (chapitre 8). Ces éléments
constituent les bases fondamentales de ce dont vous aurez besoin
pour déployer votre service dans Kubernetes.
Après ces chapitres, nous étudions quelques objets plus spécialisés
dans Kubernetes : les DaemonSets (chapitre 9), les jobs
(chapitre 10), les ConfigMaps et les secrets (chapitre 11). Bien que
ces chapitres soient essentiels pour de nombreuses applications de
production, vous pouvez les ignorer et y retourner plus tard, quand
vous aurez acquis plus d’expérience et d’expertise, si vous
commencez juste à découvrir Kubernetes.
Nous couvrons ensuite les déploiements (chapitre 12), qui mêlent le
cycle de vie d’une application complète et l’intégration du stockage
dans Kubernetes (chapitre 13). Enfin, nous concluons par quelques
exemples illustrant comment développer et déployer des
applications réelles dans Kubernetes.

◆ Ressources en ligne

Il est souhaitable d’installer Docker (https://docker.com) et de vous


familiariser avec sa documentation si ce n’est déjà fait.
De la même manière, il est préférable d’installer l’outil en ligne de
commande kubectl (https://kubernetes.io). Vous voudrez sans doute
aussi vous abonner à la chaîne Slack Kubernetes
(http://slack.kubernetes.io), où vous trouverez une grande
communauté d’utilisateurs qui sont tout disposés à répondre à vos
questions à presque n’importe quelle heure du jour et de la nuit.
Enfin, quand vous aurez bien progressé, vous pouvez participer au
dépôt open source Kubernetes sur GitHub
(https://github.com/kubernetes/kubernetes).

◆ Conventions utilisées dans ce livre

Les conventions typographiques suivantes sont utilisées dans ce


livre :
Italique
Indique les nouveaux termes, les URL, les adresses électroniques,
les noms de fichiers et les extensions de fichier.
Police à espacement fixe
Utilisée pour les listings de code des programmes, ainsi que dans
les paragraphes pour faire référence à des éléments de programme
comme des noms de variables ou de fonctions, des bases de
données, des types de donnée, des variables d’environnement, des
instructions et des mots-clés.
Police à espacement fixe en gras
Affiche les commandes ou tout autre texte qui doit être saisi
littéralement par l’utilisateur.
Police à espacement fixe en italique
Affiche le texte qui doit être remplacé par des valeurs fournies par
l’utilisateur ou par des valeurs déterminées par le contexte.

Cette icône représente une astuce, une suggestion ou


une note.

Cette icône indique un avertissement ou une mise en


garde.

◆ Utilisation des exemples de code

Les ressources supplémentaires (exemples de code, exercices, etc.)


sont téléchargeables à l’adresse https://github.com/kubernetes-up-
and-Running/examples.
Ce livre est conçu pour faciliter votre travail. En général, si un
exemple de code est proposé avec ce livre, vous pouvez l’utiliser
dans vos programmes et votre documentation. Vous n’avez pas
besoin de nous contacter pour une autorisation à moins que vous ne
reproduisiez une partie importante du code. Par exemple, l’écriture
d’un programme qui utilise plusieurs extraits de code de ce livre ne
nécessite pas d’autorisation. En revanche, la vente ou la distribution
d’un CD-ROM d’exemples extraits d’ouvrages de chez O’Reilly exige
une autorisation. Si vous répondez à une question en citant ce livre
et en citant un exemple de code, vous n’avez pas besoin d’une
d’autorisation, mais si vous incorporez beaucoup d’exemples de
code tirés de ce livre dans la documentation de votre produit, il vous
faudra demander une autorisation.
Nous apprécions, sans que cela constitue une exigence, d’être
référencés quand vous citez notre travail. Une référence inclut
généralement le titre de l’ouvrage, son auteur, l’éditeur et le numéro
ISBN. Par exemple : « Kubernetes Up and Running par Kelsey
Hightower, Brendan Burns, et Joe Beda (O’Reilly), 2017, 978-1-491-
93567-5 pour la version originale (Kubernetes Maîtriser
l’orchestrateur des infrastructures du futur, Dunod, 2019 -
9782100789405 » pour la version en français).
Si vous pensez que votre utilisation d’exemples de code dépasse le
cadre juridique de la courte citation, n’hésitez pas à nous contacter à
permissions@oreilly.com.
Nous avons une page Web pour la version originale de ce livre en
anglais, où nous répertorions les errata, les exemples et toutes les
informations supplémentaires : http://bit.ly/kubernetes-up-and-
running et sur www.dunod.com pour cette version en français.
1

Introduction

Kubernetes est un orchestrateur open source pour le déploiement


d’applications en conteneurs. Kubernetes a été développé à l’origine
par Google, qui s’est inspiré de sa longue expérience du
déploiement de systèmes évolutifs et fiables dans des conteneurs
via des API orientées application1.
Mais Kubernetes ne s’est pas contenté d’exporter simplement la
technologie développée par Google. Il a mûri pour devenir le produit
d’une riche communauté open source en expansion. Cela signifie
que Kubernetes est un produit qui est adapté non seulement aux
besoins des entreprises présentes sur Internet, mais aussi à tous les
développeurs cloud-native, qu’ils travaillent sur un cluster de
Raspberry Pi ou sur une ferme de serveurs avec des ordinateurs à
la pointe de la technologie. Kubernetes fournit le logiciel nécessaire
pour construire et déployer avec succès des systèmes distribués
fiables et évolutifs.
Vous vous demandez peut-être ce que nous entendons par
« systèmes distribués fiables et évolutifs ». De plus en plus de
services sont livrés sur le réseau via des API. Ces API sont souvent
livrées par un système distribué, les différents éléments qui
implémentent ces API fonctionnant sur différentes machines,
connectées via le réseau et coordonnant leurs actions via des
communications réseau. Dans la mesure où nous comptons de plus
en plus sur ces API pour gérer tous les aspects de notre vie
quotidienne (par exemple, trouver l’itinéraire vers l’hôpital le plus
proche), ces systèmes doivent être extrêmement fiables. Ils ne
peuvent pas se permettre de tomber en panne, même si une partie
du système plante ou fonctionne mal. De la même manière, ils
doivent rester disponibles même pendant les déploiements de
logiciels ou les opérations de maintenance. Enfin, comme de plus en
plus de monde est présent sur Internet et utilise de tels services, ils
doivent être très évolutifs afin de pouvoir accroître leur capacité pour
satisfaire une utilisation en augmentation continue sans avoir à
modifier radicalement la conception du système distribué qui
implémente les services.
Quelle que soit votre expérience en matière de conteneurs, de
systèmes distribués, et de Kubernetes, nous pensons que ce livre
vous permettra de tirer le meilleur parti de l’usage de Kubernetes.
Il existe de nombreuses raisons qui poussent les gens à utiliser des
conteneurs et des API conteneur comme Kubernetes, mais nous
estimons qu’ils cherchent tous à bénéficier au moins de l’un de ces
avantages :
Vitesse
Évolutivité (du logiciel et des équipes)
Abstraction de l’infrastructure
Efficacité
Dans les paragraphes suivants, nous décrivons la manière dont
Kubernetes peut vous aider à profiter de chacun de ces avantages.

_ 1.1 VITESSE
La rapidité est le facteur clé aujourd’hui dans presque tous les
développements informatiques. La nature du logiciel a changé et on
est passé d’un programme vendu dans une boîte contenant un CD à
des services basés sur le Web qui sont modifiés toutes les heures,
ce qui signifie que ce qui fait la différence avec vos concurrents,
c’est souvent la vitesse avec laquelle vous savez développer et
déployer de nouveaux composants et de nouvelles fonctionnalités.
Il est cependant important de noter que cette rapidité n’est pas
simplement définie en termes de vitesse brute. Même si vos
utilisateurs sont toujours à la recherche d’améliorations itératives, ils
resteront plus intéressés par un service extrêmement fiable. Par le
passé, on tolérait qu’un service soit en maintenance à minuit tous les
soirs, mais aujourd’hui, nos utilisateurs s’attendent à bénéficier d’une
disponibilité constante, même si le logiciel qu’ils exécutent est mis à
jour en permanence.
Par conséquent, la vitesse n’est pas mesurée en fonction du nombre
de mises à jour que vous pouvez livrer en une heure ou en un jour,
mais plutôt en fonction du nombre de choses que vous pouvez livrer
tout en maintenant un service hautement disponible.
C’est dans ce but que les conteneurs et Kubernetes peuvent fournir
les outils dont vous avez besoin pour réagir rapidement, tout en
restant disponibles. Les concepts de base qui permettent cela sont
l’immutabilité, la configuration déclarative, et les systèmes d’auto-
guérison en ligne. Ces idées sont toutes interdépendantes afin
d’améliorer radicalement la rapidité avec laquelle vous pouvez
déployer de manière fiable des logiciels.

1.1.1 La valeur de l’immutabilité

Les containers et Kubernetes encouragent les développeurs à créer


des systèmes distribués qui respectent les principes d’infrastructure
immutable. Avec une infrastructure immutable, une fois qu’un
artefact est créé dans le système, les modifications de l’utilisateur ne
peuvent pas le faire changer.
Traditionnellement, les ordinateurs et les systèmes logiciels sont
considérés comme des infrastructures mutables. Avec une
infrastructure mutable, les modifications sont appliquées en tant que
mises à jour incrémentielles à un système existant. Une mise à
niveau du système via l’outil apt-get update est un bon exemple de
mise à jour d’un système mutable. En exécutant apt, on télécharge
séquentiellement tous les fichiers binaires mis à jour, on les copie
par dessus des fichiers binaires plus anciens, et on effectue des
mises à jour incrémentielles des fichiers de configuration. Avec un
système mutable, l’état actuel de l’infrastructure n’est pas représenté
comme un artefact unique, mais plutôt comme une accumulation de
mises à jour et de modifications incrémentielles. Sur de nombreux
systèmes, ces mises à jour incrémentielles proviennent non
seulement des mises à niveau du système, mais aussi des
modifications de l’exploitant.
En revanche, dans un système immutable, au lieu d’une série de
mises à jour et de modifications incrémentielles, une image
entièrement nouvelle et complète est créée, et la mise à jour
remplace simplement en une seule opération la totalité de l’image
par l’image la plus récente. Il n’y a pas de modifications
incrémentielles. Comme vous pouvez l’imaginer, cela constitue un
changement significatif dans l’univers plus traditionnel de la gestion
des configurations.
Pour illustrer notre propos dans le monde des conteneurs, prenez
l’exemple de deux manières différentes de mettre à niveau votre
logiciel :
1. Vous pouvez vous connecter à un conteneur, exécuter une
commande pour télécharger votre nouveau logiciel, supprimer
l’ancien serveur et démarrer le nouveau.
2. Vous pouvez créer une nouvelle image de conteneur, la
pousser dans un registre de conteneurs, supprimer le conteneur
existant et en démarrer un nouveau.
À première vue, il peut sembler difficile de faire la différence entre
ces deux approches. En quoi le fait de créer un nouveau conteneur
peut-il améliorer la fiabilité ?
La différence principale est l’artefact que vous créez, et
l’enregistrement de la façon dont vous l’avez créé. Ces
enregistrements permettent d’identifier exactement les différences
entre les versions et, si la nouvelle version a un souci, il est facile de
déterminer ce qui a changé et comment résoudre le problème.
En outre, la construction d’une nouvelle image ne modifie pas une
image existante, ce qui signifie que l’ancienne image est toujours
présente, et qu’il est possible de l’utiliser rapidement pour une
restauration si une erreur se produit. En revanche, une fois que vous
copiez votre nouveau fichier binaire sur un fichier binaire existant,
une telle restauration est presque impossible.
Les images de conteneurs immutables sont au cœur de tout ce que
vous allez créer dans Kubernetes. Il est possible de changer grâce à
des instructions les conteneurs en cours d’exécution, mais il s’agit là
d’un anti-modèle à n’employer que dans les cas extrêmes où il n’y a
pas d’autres options (par exemple, si c’est la seule façon de réparer
temporairement un système critique de production). Et même dans
ce cas-là, les modifications doivent aussi être enregistrées par le
biais d’une mise à jour de configuration déclarative à un moment
ultérieur, quand la crise est terminée.

1.1.2 Configuration déclarative

L’immutabilité s’étend au-delà des conteneurs en cours d’exécution


dans votre cluster et s’applique aussi à la façon dont vous décrivez
votre application à Kubernetes. Tout élément dans Kubernetes est
un objet de configuration déclarative qui représente l’état désiré du
système. C’est le travail de Kubernetes de s’assurer que l’état réel
du monde correspond à cet état désiré.
Comme dans l’opposition entre infrastructure mutable et
infrastructure immutable, la configuration déclarative est une
alternative à la configuration impérative, où l’état du monde est défini
par l’exécution d’une série d’instructions plutôt que par une
déclaration de l’état désiré du monde. Alors que les commandes
impératives définissent des actions, les configurations déclaratives
définissent un état.
Pour comprendre ces deux approches, prenez l’exemple de la tâche
consistant à produire trois réplicas d’un logiciel. Avec une approche
impérative, la configuration dirait : « exécuter A, exécuter B, et
exécuter C ». La configuration déclarative correspondante serait :
« les réplicas sont au nombre de trois ».
Comme elle décrit l’état du monde, la configuration déclarative n’a
pas à être exécutée pour qu’on la comprenne. Son impact est
déclaré concrètement. Étant donné que les effets de la configuration
déclarative peuvent être compris avant son exécution, la
configuration déclarative est beaucoup moins sujette aux erreurs. En
outre, les outils traditionnels de développement de logiciels, comme
le contrôle de code source, l’examen de codes et les tests unitaires,
peuvent être utilisés dans la configuration déclarative selon des
modalités qui sont impossibles avec les instructions impératives.
La combinaison de l’état déclaratif stocké dans un système de
contrôle de versions avec la capacité de Kubernetes à faire
correspondre la réalité à cet état déclaratif facilite grandement la
restauration d’une modification. Il s’agit simplement de confirmer à
nouveau l’état déclaratif précédent du système. Avec des systèmes
impératifs, ceci est généralement impossible ; en effet, alors que les
instructions impératives décrivent comment aller d’un point A à un
point B, elles incluent rarement les instructions pour faire l’opération
en sens inverse.

1.1.3 Systèmes d’auto-guérison

Kubernetes est un système d’auto-guérison en ligne. Lorsqu’il reçoit


une configuration d’état désiré, il ne prend pas simplement des
mesures pour que l’état actuel corresponde à l’état désiré à un
instant donné, il agit de manière continue pour s’assurer que l’état
actuel corresponde en permanence à l’état désiré. Cela signifie que
non seulement Kubernetes va initialiser votre système, mais en plus
qu’il le protégera contre toute panne ou perturbation qui pourrait le
déstabiliser et affecter sa fiabilité.
Une réparation plus traditionnelle de l’exploitant implique une série
d’étapes correctives manuelles, ou une intervention humaine
effectuée en réponse à une alerte du système. Ce type de réparation
impérative est plus chère (puisqu’elle exige généralement qu’un
opérateur d’astreinte soit disponible pour assurer la réparation). Elle
est aussi habituellement plus lente, car il faut souvent réveiller une
personne qui doit se connecter pour traiter la demande
d’intervention. En outre, elle est moins fiable puisque la série
impérative d’opérations de réparation souffre de tous les problèmes
de gestion impérative que nous avons décrits dans la section
précédente. Les systèmes d’auto-guérison comme Kubernetes
réduisent la charge de travail imposée aux opérateurs et améliorent
la fiabilité globale du système en effectuant des réparations sûres
plus rapidement.
Pour prendre un exemple concret de ce comportement d’auto-
guérison, si vous déclarez un état désiré de trois réplicas à
Kubernetes, il ne crée pas seulement trois réplicas : il s’assure en
permanence qu’il y a exactement trois réplicas. Si vous créez
manuellement un quatrième réplica, Kubernetes va en détruire un
pour ramener à trois le nombre de réplicas. Si vous détruisez
manuellement un réplica, Kubernetes va en créer un pour retourner
à nouveau à l’état désiré de trois.
Les systèmes d’auto-guérison en ligne améliorent la productivité du
développeur, car le temps et l’énergie que vous ne consacrez pas
aux opérations de maintenance peuvent être investis pour
développer et tester de nouvelles fonctionnalités.

_ 1.2 ÉVOLUTIVITÉ DE VOTRE SERVICE


ET DE VOS ÉQUIPES
Au fur et à mesure de l’évolution de la croissance de votre produit, il
est inévitable que vous ayez besoin de redimensionner à la fois
votre logiciel et les équipes qui le développent. Heureusement,
Kubernetes peut vous aider à atteindre ces deux objectifs.
Kubernetes parvient à l’évolutivité en favorisant les architectures
découplées.

1.2.1 Découplage

Dans une architecture découplée, chaque composant est séparé des


autres composants par des API définies et des équilibreurs de
charge de services. Les API et les équilibreurs de charge isolent
chaque partie du système des autres parties. Les API fournissent
une mémoire tampon entre l’implémenteur et le consommateur, et
les équilibreurs de charge fournissent une mémoire tampon entre les
instances en cours d’exécution de chaque service.
Les composants de découplage via les équilibreurs de charge
facilitent l’évolutivité des programmes qui composent votre service,
car l’augmentation de la taille (et donc de la capacité) du programme
peut se faire sans ajuster ni reconfigurer les autres couches de votre
service.
Le découplage des serveurs via les API facilite l’évolutivité des
équipes de développement car chaque équipe peut se concentrer
sur un seul micro-service plus petit avec une surface plus facile à
appréhender. Les API claires et nettes entre les micro-services
fluidifient la communication nécessaire entre les équipes pour créer
et déployer les logiciels. L’inflation de la communication entre les
équipes est souvent le principal facteur qui limite leur évolutivité.

1.2.2 Faciliter l’évolutivité des applications


et des clusters

Concrètement, lorsque vous avez besoin de faire évoluer votre


service, la nature immutable et déclarative de Kubernetes facilite
l’implémentation de cette mise à l’échelle. Étant donné que vos
conteneurs sont immutables et que le nombre de réplicas n’est qu’un
chiffre dans une configuration déclarative, l’évolutivité de votre
service est simplement une question de modification d’un nombre
dans un fichier de configuration, et de déclaration de ce nouvel état
à Kubernetes qui va s’occuper du reste tout seul. Vous pouvez
également configurer l’évolutivité automatique et laisser simplement
Kubernetes gérer cela à votre place.
Bien sûr, ce type d’évolutivité suppose qu’il y ait des ressources
disponibles dans votre cluster, et parfois vous aurez besoin
d’agrandir le cluster lui-même. Ici encore, Kubernetes facilite cette
tâche. Comme chaque machine dans un cluster est parfaitement
identique à chaque autre machine, et que les applications elles-
mêmes sont découplées des détails de la machine grâce à des
conteneurs, l’ajout de ressources supplémentaires dans un cluster
n’est qu’une simple question de création d’image d’une nouvelle
machine à joindre au cluster. Ceci peut être accompli grâce à
quelques commandes simples ou par l’emploi d’une image de
machine préconstruite.
L’un des défis de l’évolutivité des ressources machine est de prévoir
leur utilisation. Si vous utilisez une infrastructure physique, le temps
d’obtention d’une nouvelle machine se mesure en jours ou en
semaines. Sur les infrastructures physiques et à la fois sur les
infrastructures dans le cloud, il est difficile de prévoir les coûts car il
n’est pas aisé d’estimer les besoins de croissance et d’évolutivité
des applications spécifiques.
Kubernetes peut simplifier la prévision des coûts de calcul. Pour
comprendre comment cela fonctionne, imaginez que vous souhaitiez
faire évoluer trois équipes. L’expérience vous a prouvé que la
croissance de chaque équipe est très variable et donc difficile à
prévoir. Si vous provisionnez des machines individuelles pour
chaque service, vous n’avez pas d’autre choix que de faire une
prévision en fonction de la croissance maximale attendue pour
chaque service, puisque les machines dédiées à une équipe ne
peuvent pas être utilisées par une autre équipe. Si au lieu de cela,
vous utilisez Kubernetes pour découpler les équipes des machines
spécifiques qu’elles utilisent, vous pouvez prévoir une croissance
basée sur la croissance globale des trois services. La combinaison
de trois taux de croissance variables en un seul taux de croissance
réduit le bruit statistique et produit une prévision plus fiable de la
croissance attendue. En outre, le découplage des équipes à partir de
machines spécifiques signifie que les équipes peuvent partager des
parties fractionnaires des machines des autres équipes, ce qui réduit
encore plus la surcharge associée à la prévision de la croissance
des ressources informatiques.

1.2.3 Évolutivité des équipes de développement grâce


à des micro-services

Comme de nombreuses études l’indiquent, la taille idéale d’une


équipe est celle qui se contente de deux pizzas2, soit environ six à
huit personnes, parce qu’un groupe de cette taille se traduit souvent
par un bon partage des connaissances, une prise de décision
rapide, et un sens commun de l’objectif à atteindre. Les plus grosses
équipes ont tendance à souffrir de problèmes hiérarchiques, d’un
manque de visibilité et de querelles qui entravent l’agilité et le
succès.
Cependant, de nombreux projets nécessitent un accroissement
significatif de leurs ressources pour réussir et atteindre leurs
objectifs. Par conséquent, il existe une tension entre la taille idéale
pour que l’équipe soit agile et la taille nécessaire pour qu’elle puisse
satisfaire les objectifs du produit.
La solution courante à cette tension a été le développement
d’équipes découplées, orientées services, qui construisent chacune
un micro-service unique. Chaque petite équipe est responsable de la
conception et de la livraison d’un service qui est consommé par
d’autres petites équipes. L’agrégation de tous ces services fournit au
bout du compte l’implémentation du périmètre global du produit.
Kubernetes fournit de nombreuses abstractions et des API qui
facilitent la construction de ces architectures de micro-services
découplées.
Les pods, qui sont des groupes de conteneurs, peuvent regrouper
des images de conteneurs développées par différentes équipes
en une seule unité déployable.
Les services Kubernetes fournissent l’équilibrage de charge, le
nommage et la découverte pour isoler un micro-service d’un
autre.
Les espaces de noms fournissent l’isolation et le contrôle d’accès,
de sorte que chaque micro-service peut contrôler le degré avec
lequel il interagit avec les autres services.
Les objets Ingress fournissent une interface facile à utiliser qui
peut combiner plusieurs micro-services en une seule surface
d’API externalisée.
Enfin, le découplage de l’image du conteneur d’application et de la
machine signifie que différents micro-services peuvent cohabiter sur
la même machine sans interférer les uns avec les autres, ce qui
réduit la surcharge et le coût des architectures de micro-services.
Les fonctionnalités de contrôle d’intégrité et de mise en œuvre de
Kubernetes assurent une approche cohérente du déploiement et de
la fiabilité des applications, ce qui garantit que la prolifération des
équipes de micro-services n’entraîne pas une multiplication de
différentes approches concernant le cycle de vie et les opérations de
production des services.

1.2.4 Séparation des problèmes en matière


de cohérence et d’évolutivité

En plus de la cohérence que Kubernetes apporte à vos opérations,


le découplage et la séparation des problèmes produits par la pile
Kubernetes engendrent une cohérence beaucoup plus grande des
niveaux inférieurs de votre infrastructure. Cela permet à votre
service d’exploitation d’évoluer vers la gestion de nombreuses
machines avec une seule équipe réduite et concentrée sur ses
objectifs. Nous avons longuement parlé du découplage du conteneur
d’applications et de la machine/système d’exploitation, mais un
aspect important de ce découplage est que l’API d’orchestration de
conteneurs devient un contrat clair qui sépare les responsabilités de
l’opérateur d’applications de celles de l’opérateur d’orchestration de
clusters. On appelle ça la ligne « c’est pas mon problème ». Le
développeur d’applications s’appuie sur l’accord de niveau de
service (SLA) fourni par l’API de l’orchestration de conteneurs, sans
se soucier des détails de la façon dont ce SLA est atteint. De la
même manière, l’ingénieur responsable de la fiabilité de l’API
d’orchestration des conteneurs se concentre sur la fourniture du SLA
de l’API d’orchestration sans se soucier des applications qui
s’exécutent au-dessus.
Ce découplage des problèmes signifie qu’une petite équipe qui
exploite un cluster Kubernetes peut être responsable de centaines,
voire de milliers d’équipes exécutant des applications dans ce
cluster (figure 1.1). De la même manière, une petite équipe peut être
responsable de l’exploitation de dizaines (ou plus) de clusters à
travers le monde. Il est important de noter que le même découplage
de conteneurs et de systèmes d’exploitation permet aux ingénieurs
responsables de la fiabilité du système d’exploitation de se
concentrer sur le SLA de l’OS des machines individuelles. Cela
permet un autre degré de partage des responsabilités, les
opérateurs Kubernetes s’appuyant sur les SLA des OS, et les
opérateurs du système d’exploitation ayant pour seule préoccupation
de fournir ce SLA. Encore une fois, cela vous permet de gérer une
petite équipe d’experts des OS capable d’exploiter une flotte de
milliers de machines.

Figure 1.1 – Illustration de la manière dont les différentes équipes


d’exploitation sont découplées à l’aide des API.

Bien évidemment, le fait de consacrer une petite équipe, si réduite


soit-elle, à la gestion d’un système d’exploitation dépasse les
capacités de nombreuses organisations. Dans ces environnements,
une architecture KaaS (Kubernetes-as-a-Service) proposée par un
fournisseur de cloud public est une excellente option.
Au moment où nous avons écrit ce livre, il était possible
d’utiliser une architecture KaaS sur Microsoft Azure, avec Azure
Container Service, ainsi que sur la plateforme Google Cloud via
Google Container Engine (GCE). Il n’existe aucun service
équivalent disponible sur Amazon Web Services (AWS), bien
que le projet kops fournisse des outils facilitant l’installation et la
gestion de Kubernetes sur AWS (voir la section « Installation de
Kubernetes sur Amazon Web Services »).

La décision soit d’utiliser KaaS soit de gérer Kubernetes vous-même


est un choix que chaque utilisateur doit faire en fonction des
compétences et des exigences de sa situation. Pour les petites
organisations, KaaS fournit souvent une solution facile à utiliser qui
leur permet de concentrer leur temps et leur énergie à la création du
logiciel plutôt qu’à la gestion d’un cluster. Pour une organisation plus
grande qui peut se permettre d’avoir une équipe dédiée à la gestion
de son cluster Kubernetes, il peut être judicieux de l’administrer par
ses propres moyens, car cela permet une plus grande souplesse en
termes de capacité et d’exploitation du cluster.

_ 1.3 ABSTRACTION DE VOTRE


INFRASTRUCTURE
L’objectif du cloud public est de fournir une infrastructure libre-
service et facile à utiliser pour les développeurs. Toutefois, trop
souvent, les API cloud sont orientées vers une mise en miroir des
infrastructures demandée par les DSI, et non pas vers des concepts
(par exemple, « machines virtuelles » en lieu et place des
« applications ») que les développeurs veulent exploiter. En outre,
dans de nombreux cas, le cloud est livré avec des détails particuliers
dans la mise en œuvre ou les services qui sont spécifiques au
fournisseur de cloud. L’exploitation directe de ces API rend difficile
l’exécution de votre application dans plusieurs environnements, ou la
propagation entre le cloud et les environnements physiques.
Le passage aux API de conteneurs orientées application comme
Kubernetes a deux avantages concrets. Tout abord, comme nous
l’avons décrit précédemment, cela sépare les développeurs des
machines spécifiques. Cela simplifie non seulement le rôle des
services informatiques orientés machine, puisque les machines
peuvent être ajoutées simplement dans l’agrégat pour faire évoluer
le cluster, mais dans le contexte du cloud, cela permet aussi un
degré élevé de portabilité puisque les développeurs consomment
une API de niveau supérieur qui est implémentée en termes d’API
d’infrastructure de cloud spécifiques.
Lorsque vos développeurs créent leurs applications en termes
d’images de conteneurs et les déploient en termes d’API Kubernetes
portables, le transfert de votre application entre des environnements,
ou même son exécution dans des environnements hybrides, est une
simple question d’envoi de configuration déclarative vers un
nouveau cluster. Kubernetes a un certain nombre de plug-in qui
permettent de vous abstraire d’un cloud particulier. Par exemple, les
services Kubernetes savent comment créer des équilibreurs de
charge sur tous les principaux clouds publics ainsi que sur plusieurs
infrastructures privées et physiques différentes. De la même
manière, les objets Kubernetes PersistentVolumes et
PersistentVolumeClaims peuvent être utilisés pour faire abstraction
de vos applications en ignorant les implémentations de stockage
spécifiques. Bien entendu, pour atteindre cette transférabilité, vous
devez éviter les services gérés par le cloud (par exemple,
DynamoDB d’Amazon ou Cloud Spanner de Google), ce qui signifie
que vous serez obligé de déployer et de gérer des solutions de
stockage open source comme Cassandra, MySQL ou MongoDB.
En combinant tout cela et en créant par-dessus les abstractions
orientées application de Kubernetes, vous vous assurez que l’effort
que vous consentez dans la construction, le déploiement et la
gestion de votre application la rend vraiment portable dans une
grande variété d’environnements.
_ 1.4 EFFICACITÉ
En plus des avantages en matière de gestion pour le développeur et
le service informatique que procurent les conteneurs et Kubernetes,
il y a aussi un bénéfice économique concret pour l’abstraction.
Comme les développeurs ne pensent plus en termes de machines,
leurs applications peuvent être situées sur les mêmes machines
sans impact sur les applications elles-mêmes. Cela signifie que les
tâches de plusieurs utilisateurs peuvent être gérées sur moins de
machines.
L’efficacité peut être mesurée par le rapport entre le travail utile
effectué par une machine (ou un processus) et la quantité totale
d’énergie dépensée. Lorsqu’il s’agit de déployer et de gérer des
applications, de nombreux outils et processus disponibles (par
exemple, des scripts bash, les mises à jour apt ou la gestion de
configuration impérative) sont assez inefficaces. Quand on réfléchit
au concept d’efficacité, il est souvent utile de penser à la fois au coût
de l’exécution d’un serveur et au coût humain nécessaire à sa
gestion.
L’exécution d’un serveur entraîne un coût basé sur l’utilisation
d’énergie, les besoins de climatisation, l’espace du data center et la
puissance brute de calcul. Une fois qu’un serveur est mis en rack et
sous tension, le compteur commence à tourner. Tout temps mort du
CPU est de l’argent gaspillé. Cela fait donc maintenant partie des
responsabilités de l’administrateur système que de maintenir
l’utilisation à des niveaux acceptables, ce qui nécessite une gestion
permanente. C’est là que les conteneurs et le workflow Kubernetes
ont un rôle à jouer. Kubernetes fournit des outils qui automatisent la
distribution d’applications à travers un cluster de machines, ce qui
assure des niveaux d’utilisation plus élevés qu’avec l’outillage
traditionnel.
Une nouvelle augmentation de l’efficacité provient du fait que
l’environnement de test d’un développeur peut être rapidement créé
pour un coût réduit sous la forme d’un ensemble de conteneurs
s’exécutant dans une vue personnelle d’un cluster Kubernetes
partagé (en utilisant une fonctionnalité appelée espaces de noms).
Par le passé, la mise en place d’un cluster de test pour un
développeur pouvait impliquer l’activation de trois machines. Avec
Kubernetes, tous les développeurs peuvent partager simplement un
cluster de test unique, en agrégeant leur utilisation sur un ensemble
de machines beaucoup plus petit. La réduction du nombre total de
machines utilisées augmente à son tour l’efficacité de chaque
système : comme plus de ressources (CPU, RAM, etc.) sur chaque
machine sont utilisées, le coût global de chaque conteneur devient
beaucoup plus faible.
La réduction du coût des instances de développement dans votre
pile autorise des pratiques de développement dont le coût aurait été
auparavant prohibitif. Par exemple, avec votre application déployée
via Kubernetes, il devient concevable de déployer et de tester
chaque modification de code de chaque développeur sur toute la
pile.
Lorsque le coût de chaque déploiement est mesuré en fonction d’un
petit nombre de conteneurs, plutôt qu’en multipliant les machines
virtuelles complètes, le coût des tests est beaucoup plus faible. Si
l’on revient à la valeur originale de Kubernetes, cette augmentation
des tests accroît aussi la vitesse, puisque vous avez à la fois des
signaux forts sur la fiabilité de votre code et sur la granularité des
détails nécessaires à l’identification rapide de l’origine des
problèmes.

_ 1.5 RÉSUMÉ
Kubernetes a été construit pour changer radicalement la façon dont
les applications sont créées et déployées dans le cloud.
Fondamentalement, il a été conçu pour donner aux développeurs
plus de rapidité, d’efficacité et d’agilité. Nous espérons que les
sections précédentes vous auront incité à déployer vos applications
à l’aide de Kubernetes. Maintenant que vous êtes convaincu, les
chapitres suivants vont vous apprendre la manière de déployer votre
application.
2

Création et exécution
de conteneurs

Kubernetes est une plateforme de création, de déploiement et de


gestion d’applications distribuées. Ces applications se présentent
sous de nombreuses formes et tailles différentes, mais en fin de
compte, elles sont toutes composées d’une ou plusieurs applications
qui fonctionnent sur des machines individuelles. Ces applications
reçoivent des données, les manipulent, puis renvoient les résultats.
Avant d’envisager de construire un système distribué, nous devons
d’abord étudier la manière de créer les images de conteneur
d’application qui constituent les pièces de notre système distribué.
Les applications sont généralement composées d’un runtime de
langage, de bibliothèques et de votre code source. Dans de
nombreux cas, votre application exploite des bibliothèques externes
comme libc et libssl. Ces bibliothèques externes sont en général
fournies sous la forme de composants partagés dans le système
d’exploitation que vous avez installé sur une machine particulière.
Des problèmes surviennent quand une application développée sur
l’ordinateur portable d’un programmeur a une dépendance avec une
bibliothèque partagée qui n’est pas disponible lorsque le programme
est déployé sur le système d’exploitation de production. Même
lorsque les environnements de développement et de production
partagent exactement la même version du système d’exploitation,
des problèmes peuvent survenir lorsque les développeurs oublient
d’inclure des fichiers de ressources à l’intérieur d’un package qu’ils
déploient sur la machine de production.
Un programme ne peut s’exécuter correctement que s’il peut être
déployé de manière fiable sur l’ordinateur où il doit s’exécuter. Trop
souvent, les méthodes actuelles de déploiement impliquent
l’exécution de scripts impératifs, qui produisent inévitablement des
pannes difficiles à détecter.
Enfin, les méthodes traditionnelles d’exécution de plusieurs
applications sur une seule machine requièrent que tous ces
programmes utilisent conjointement les mêmes versions des
bibliothèques partagées sur le système. Si les différentes
applications sont développées par différentes équipes ou
organisations, ces dépendances partagées ajoutent une complexité
inutile et la nécessité d’une communication entre ces équipes.
Dans le premier chapitre, nous avons souligné l’intérêt des images
et des infrastructures immutables. Il s’avère que l’image d’un
conteneur procure exactement les mêmes avantages. Comme nous
le verrons, cette technique résout avec élégance tous les problèmes
de gestion des dépendances et d’encapsulation que nous venons de
décrire.
Lorsque vous travaillez avec des applications, il est souvent utile de
les packager de manière à faciliter leur partage. Docker, le moteur
d’exécution de conteneurs par défaut, facilite l’empaquetage d’une
application qui sera ensuite déposée dans un registre distant où elle
pourra être récupérée ultérieurement.
Dans ce chapitre, nous exploitons un exemple simple d’application
que nous avons créée pour ce livre afin de vous montrer comment
fonctionne ce workflow. Vous pouvez télécharger l’application sur
GitHub (https://github.com/kubernetes-up-and-running/kuard).
Les images de conteneurs regroupent une application et ses
dépendances, sous un système de fichiers racine, en un seul
artefact. Le format d’image de conteneur le plus populaire est le
format d’image Docker, qui est le principal format d’image pris en
charge par Kubernetes. Les images Docker incluent également des
métadonnées supplémentaires qui sont utilisées par un runtime de
conteneur pour démarrer l’exécution d’une instance d’application en
fonction du contenu de l’image du conteneur.
Ce chapitre traite des sujets suivants :
Comment packager une application avec le format d’image Docker
Comment démarrer une application à l’aide du runtime de conteneur Docker

_ 2.1 IMAGES DE CONTENEURS


La plupart des gens découvrent la technologie de conteneur grâce à
une image de conteneur. Une image de conteneur est un paquet
binaire qui encapsule tous les fichiers nécessaires à l’exécution
d’une application à l’intérieur d’un conteneur de système
d’exploitation. Vous pouvez créer une image de conteneur à partir de
votre système de fichiers local ou bien télécharger une image
préexistante à partir d’un registre de conteneurs. Dans les deux cas,
une fois que l’image du conteneur est présente sur votre ordinateur,
vous pouvez exécuter cette image afin de produire une application
fonctionnelle à l’intérieur d’un conteneur de système d’exploitation.

2.1.1 Le format d’image Docker

Le format d’image de conteneur le plus populaire et le plus répandu


est le format d’image Docker, qui a été développé par le projet open
source Docker pour packager, distribuer, et exécuter des conteneurs
à l’aide de la commande docker. Ensuite, la société Docker et
d’autres entreprises ont entamé un travail pour standardiser le
format d’image de conteneur au sein du projet OCI (Open Container
Initiative). L’OCI a publié à la mi-2017 la première version de sa
norme, mais son adoption est encore très récente. Le format
d’image Docker, qui est toujours le standard de fait, est composé
d’une série de couches de système de fichiers. Chaque couche
ajoute, supprime ou modifie des fichiers de la couche précédente du
système de fichiers. Il s’agit d’un exemple de système de fichiers par
overlays. On compte une grande variété d’implémentations
concrètes de ces systèmes de fichiers, notamment aufs, overlay et
overlay2.
Stratification des conteneurs
Les images de conteneurs sont constituées d’une série de
couches de systèmes de fichiers, où chaque couche hérite des
couches précédentes qu’elle modifie. Pour vous aider à
comprendre cela en détail, nous allons créer des conteneurs.
Vous noterez que l’ordre des couches devrait aller du bas vers
le haut, mais pour des raisons pédagogiques, nous présentons
les choses dans le sens inverse :
.
└── conteneur A : il ne s’agit que d’un système d’exploitation de base, par
exemple Debian
└── conteneur B : construit à partir de #A, en ajoutant Ruby v2.1.10
└── conteneur C : construit à partir de #A, en ajoutant Golang v1.6

À ce stade, nous avons trois conteneurs : A, B et C. Les


conteneurs B et C dérivent de A et ne partagent rien en dehors
des fichiers du conteneur de base. Si l’on poursuit, nous
pouvons créer un conteneur à partir de B en ajoutant Rails
(version 4.2.6). Nous pouvons aussi souhaiter exploiter une
ancienne application qui nécessite une version moins récente
de Rails (par exemple, la version 3.2.x). Nous pouvons alors
construire une image de conteneur pour prendre en charge
cette application basée sur B, avec le projet de migrer
ultérieurement l’application en version 4 :
. (suite du schéma précédent)
└── conteneur B : construit à partir de #<A, en ajoutant Ruby v2.1.10
└── conteneur D : construit à partir de #B, en ajoutant Rails v4.2.6
└── conteneur E : construit à partir de #B, en ajoutant Rails v3.2.x

Conceptuellement, chaque couche d’image de conteneur


s’appuie sur la précédente. Chaque référence parente est un
pointeur. Alors que l’exemple illustré ici est un ensemble simple
de conteneurs, les véritables conteneurs peuvent faire partie
d’un graphe orienté acyclique plus étendu.
Les images de conteneurs sont en général combinées avec un
fichier de configuration de conteneur, qui fournit des instructions sur
la façon de configurer l’environnement du conteneur et d’exécuter le
point d’entrée de l’application. La configuration du conteneur
comprend aussi souvent des informations sur la façon de paramétrer
la mise en réseau, l’isolation des espaces de noms, les contraintes
de ressources (cgroups), et les restrictions syscall à appliquer sur
une instance de conteneur en cours exécution. Le système de
fichiers racine du conteneur et le fichier de configuration sont
généralement regroupés en employant le format d’image Docker.
Les conteneurs se divisent en deux catégories principales :
les conteneurs système
les conteneurs d’application
Les conteneurs système cherchent à imiter les machines virtuelles et
exécutent souvent un processus de démarrage complet. Ils incluent
souvent un ensemble de services système que l’on trouve
d’habitude dans une machine virtuelle, comme ssh, cron et syslog.
Les conteneurs d’application diffèrent des conteneurs système en ce
qu’ils exécutent généralement une seule application. Bien que
l’exécution d’une seule application par conteneur puisse paraître
inutilement contraignante, cela offre un niveau parfait de granularité
pour la composition évolutive applications, et il s’agit d’une
philosophie de conception qui est fortement exploitée par les pods.

_ 2.2 CRÉATION D’IMAGES


D’APPLICATION AVEC DOCKER
En général, les systèmes d’orchestration de conteneurs comme
Kubernetes sont axés sur la création et le déploiement de systèmes
distribués constitués de conteneurs d’applications. C’est la raison
pour laquelle nous nous concentrerons sur les conteneurs
d’application dans le reste de ce chapitre.
2.2.1 Dockerfiles

Un Dockerfile peut être utilisé pour automatiser la création d’une


image de conteneur Docker. L’exemple suivant décrit les étapes
nécessaires pour générer l’image kuard, qui est à la fois sécurisée et
d’une taille réduite :
FROM alpine
MAINTAINER Kelsey Hightower <kelsey.hightower@kuar.io>
COPY bin/1/amd64/kuard /kuard
ENTRYPOINT ["/kuard"]

Ce code peut être stocké dans un fichier texte, que l’on nomme en
général Dockerfile et qui est utilisé pour créer une image Docker.
Pour commencer, vous devez compiler les fichiers binaires de kuard.
Pour cela, vous pouvez exécuter make dans le répertoire kuard.
Exécutez la commande suivante pour créer l’image Docker kuard :
$ docker build -t kuard-amd64:1 .

Nous avons choisi de créer l’image en nous basant sur Alpine, qui
est une distribution Linux minimaliste. Par conséquent, l’image finale
devrait peser environ 6 Mo, ce qui est une taille beaucoup plus
restreinte que la plupart des images disponibles qui ont tendance à
être construites à partir de versions plus complètes de systèmes
d’exploitation comme Debian.
À ce stade, notre image kuard réside dans le registre local Docker
où l’image a été construite et où elle n’est accessible qu’à une seule
machine. La véritable puissance de Docker provient de sa capacité à
partager des images à travers des milliers de machines et la vaste
communauté Docker.

2.2.2 Sécurité des images

En matière de sécurité, il n’y a pas de demi-mesures. Lorsque vous


créez des images qui s’exécuteront à la fin dans un cluster de
production Kubernetes, assurez-vous de suivre les meilleures
pratiques pour le packaging et la distribution des applications. Par
exemple, ne créez pas de conteneurs avec des mots de passe
recyclés, cette précaution s’appliquant non seulement à la dernière
couche, mais aussi à toutes les couches de l’image. Cela peut
sembler paradoxal, mais un des problèmes introduits par les
couches des conteneurs réside dans le fait que la suppression d’un
fichier dans une couche ne supprime pas ce fichier dans les couches
précédentes. Il occupe encore de l’espace et on peut y accéder en
employant les bons outils (un hacker motivé peut simplement créer
une image qui se compose uniquement des couches contenant le
mot de passe).
Il ne faut jamais mélanger les secrets et les images. Si vous faites
cela, vous serez piraté, et la honte rejaillira sur votre entreprise ou
votre service. Nous voulons tous passer un jour à la télévision, mais
il y a sans doute des moyens à éviter.

2.2.3 Optimisation de la taille des images

Il y a plusieurs pièges dans lesquels peuvent tomber les gens qui


commencent à travailler avec des images de conteneur, notamment
le fait d’engendrer des images trop grandes. La première chose à
retenir est que les fichiers qui sont supprimés par les couches
successives dans le système sont en fait encore présents dans les
images ; ils sont simplement inaccessibles. Examinez la situation
suivante :
.
└── couche A : contient un gros fichier nommé 'GrosFichier'
└── couche B : supprime 'GrosFichier'
└── couche C : créé à partir de B, en ajoutant un binaire statique

Vous pensez sans doute que GrosFichier n’est plus présent dans
cette image. Après tout, lorsque vous exécutez l’image, il n’est plus
accessible. Mais en fait il est toujours présent dans la couche A, ce
qui signifie que chaque fois que vous voulez envoyer ou extraire
l’image, GrosFichier est toujours transmis par le réseau, même si
vous ne pouvez plus y accéder.
Il y a un autre écueil qui concerne la mise en cache et la
construction des images. Vous devez vous rappeler que chaque
couche est différente et indépendante de la couche qui est en
dessous. Chaque fois que vous modifiez une couche, cela change
les couches qui viennent après. Si vous modifiez les couches
précédentes, cela signifie qu’il faut les reconstruire, les renvoyer et
les extraire pour déployer votre image vers l’environnement de
développement.
Pour mieux comprendre cela, comparons ces deux images :
.
└── couche A : contient un système d’exploitation de base
└── couche B : ajoute le code source server.js
└── couche C : installe le package 'node'

et :
.
└── couche A : contient un système d’exploitation de base
└── couche B : installe le package 'node'
└── couche C : ajoute le code source server.js

Il semble évident que ces deux images se comportent de manière


identique, et c’est effectivement le cas la première fois où elles sont
extraites. Cependant, réfléchissez à ce qui se passe quand server.js
est modifié. Dans un cas, il s’agit seulement d’une modification qui
doit être envoyée ou extraite, mais dans l’autre cas, à la fois
server.js et la couche fournissant le package node doivent être
extraits et envoyés, puisque la couche node est dépendante de la
couche server.js. En général, il est souhaitable d’organiser ses
couches en partant de celle qui est la moins susceptible d’être
modifiée jusqu’à celle qui a le plus de risque de changer afin
d’optimiser la taille de l’image à envoyer ou à extraire.
_ 2.3 STOCKAGE D’IMAGES DANS
UN REGISTRE DISTANT
À quoi sert une image de conteneur si elle n’est disponible que sur
une seule machine ?
Kubernetes repose sur le fait que les images décrites dans un
manifeste de pod sont disponibles sur chaque machine du cluster.
Pour récupérer cette image sur toutes les machines du cluster, on
pourrait exporter l’image kuard et l’importer sur toutes les autres
machines du cluster Kubernetes. Gérer des images Docker de cette
façon-là serait cependant extrêmement fastidieux. Le processus
manuel d’importation et d’exportation d’images Docker comporte
trop de risques d’erreur humaine et il vaut mieux ne pas y penser.
La norme au sein de la communauté Docker consiste à stocker des
images dans un registre distant. Il existe de très nombreuses options
en matière de registre Docker, et vos choix seront guidés par vos
exigences de sécurité et vos besoins de fonctionnalités
collaboratives.
En règle générale, le premier choix que vous devez faire est de
savoir si vous voulez utiliser un registre privé ou un registre public.
Les registres publics permettent à toute personne de télécharger les
images stockées dans le registre, tandis que les registres privés
nécessitent une authentification pour le téléchargement. Votre choix
dépendra de l’utilisation que vous voulez faire de votre conteneur.
Les registres publics sont parfaits pour partager des images dans le
monde entier, parce qu’ils permettent une utilisation facile et non
authentifiée des images de conteneur. Vous pouvez aisément
distribuer votre logiciel sous la forme d’une image de conteneur et
avoir la certitude que les utilisateurs bénéficieront partout de la
même expérience.
En revanche, un registre privé est préférable pour stocker vos
applications qui ne sont destinées qu’à votre service et que vous ne
voulez pas partager avec tout le monde.
Quoi qu’il en soit, pour envoyer une image, vous devez vous
authentifier sur le registre. Vous pouvez généralement le faire avec
la commande docker login, bien qu’il y ait quelques différences avec
certains registres. Dans les exemples présentés ici, nous envoyons
les images sur le registre Google Cloud Platform, appelé GCR
(Google Container Registry). Pour les nouveaux utilisateurs
hébergeant des images lisibles publiquement, le Docker Hub
(https://hub.docker.com) est un excellent endroit pour débuter.
Une fois connecté, vous pouvez attribuer une balise à l’image kuard
en ajoutant le registre Docker cible :
$ docker tag kuard-amd64:1 gcr.io/kuar-demo/kuard-amd64:1

Ensuite, vous pouvez envoyer l’image kuard :


$ docker push gcr.io/kuar-demo/kuard-amd64:1

Maintenant que l’image kuard est disponible sur un registre distant, il


est temps de la déployer à l’aide de Docker. Comme nous l’avons
envoyée sur un registre Docker public, elle sera disponible partout
sans authentification.

_ 2.4 LE RUNTIME DE CONTENEUR


DOCKER
Kubernetes fournit une API pour décrire un déploiement
d’application, mais il repose sur un runtime de conteneur pour
configurer un conteneur d’application en utilisant des API spécifiques
au conteneur qui sont natives du système d’exploitation cible. Sur un
système Linux, cela signifie qu’il faut configurer cgroups et les
espaces de noms.
Le runtime de conteneur par défaut utilisé par Kubernetes est
Docker. Docker fournit une API pour créer des conteneurs
d’application sur les systèmes Linux et Windows.
2.4.1 Exécution de conteneurs avec Docker

L’outil CLI Docker peut être utilisé pour déployer des conteneurs.
Pour déployer un conteneur à partir de l’image gcr.io/kuar-
demo/kuard-amd64:1, exécutez la commande suivante :
$ docker run -d --name kuard \
--publish 8080:8080 \
gcr.io/kuar-demo/kuard-amd64:1

Cette commande démarre la base de données kuard et mappe les


ports 8080 de votre ordinateur local sur les ports 8080 du conteneur.
C’est parce que chaque conteneur obtient sa propre adresse IP, que
l’écoute sur localhost à l’intérieur du conteneur ne permet pas
l’écoute sur votre machine. Sans le transfert de port, les connexions
seront inaccessibles sur votre machine.

2.4.2 Découverte de l’application kuard

kuard possède une interface Web simple, que vous pouvez charger
en pointant votre navigateur à http://localhost:8080 ou via la ligne de
commande :
$ curl http://localhost:8080

kuard propose aussi un certain nombre de fonctions intéressantes


que nous découvrirons plus tard dans ce livre.

2.4.3 Limitation de l’utilisation des ressources

Docker fournit la possibilité de limiter la quantité de ressources


utilisées par les applications en proposant la technologie sous-
jacente cgroup offerte par le noyau Linux.

◆ Limitation des ressources de mémoire


L’un des principaux avantages de l’exécution d’applications dans un
conteneur est la possibilité de restreindre l’utilisation des ressources.
Ceci permet à plusieurs applications de coexister sur le même
matériel tout en garantissant une utilisation équitable des
ressources.
Pour limiter kuard à 200 Mo de mémoire et 1 Go d’espace de swap,
utilisez les options --memory et --memory-swap avec la commande
docker run.
Pour arrêter et supprimer le conteneur kuard en cours d’utilisation :
$ docker stop kuard
$ docker rm kuard

Démarrez ensuite un autre conteneur kuard en utilisant les options


appropriées pour limiter l’utilisation de la mémoire :
$ docker run -d --name kuard \
--publish 8080:8080 \
--memory 200m \
--memory-swap 1G \
gcr.io/kuar-demo/kuard-amd64:1

◆ Limitation des ressources CPU

Le CPU est aussi une ressource critique sur une machine. Vous
pouvez restreindre l’utilisation du CPU à l’aide de l’option --cpu-
shares avec la commande docker run :
$ docker run -d --name kuard \
--publish 8080:8080 \
--memory 200m \
--memory-swap 1G \
--cpu-shares 1024 \
gcr.io/kuar-demo/kuard-amd64:1

_ 2.5 NETTOYAGE
Une fois que vous en avez terminé avec une image, vous pouvez la
supprimer avec la commande docker rmi :
docker rmi <nom de balise>

ou
docker rmi <identificateur d’image>

Les images peuvent être supprimées en utilisant leur nom de balise


(par exemple, gcr.io/kuar-demo/kuardamd64:1) ou leur identificateur
d’image. Comme pour toutes les valeurs d’identificateur de l’outil
docker, l’identificateur d’image peut être raccourci tant qu’il reste
unique. En général, les trois ou quatre premiers caractères de
l’identificateur suffisent.
Il est important de noter que, sauf si vous supprimez explicitement
une image, elle demeure à jamais sur votre système, même si vous
créez une nouvelle image avec un nom identique. La création de
cette nouvelle image déplace simplement la balise sur la nouvelle
image ; cela ne supprime pas ou ne remplace pas l’ancienne image.
Cela a pour conséquence qu’au fur et à mesure que vous créez de
nouvelles images, vous créez souvent de nombreuses images
différentes qui finissent par occuper inutilement de l’espace sur votre
ordinateur.
Pour voir les images présentes sur votre machine, vous pouvez
utiliser la commande docker images. Vous pouvez ensuite supprimer
les balises que vous n’utilisez plus.
Une approche un peu plus sophistiquée consiste à mettre en place
un job cron pour exécuter un garbage collector d’images. Par
exemple, l’outil docker_gc (https://github.com/spotify/dockergc) est
un outil de suppression d’images couramment utilisé et qui peut être
facilement utilisé de manière récurrente sous la forme d’un job cron,
une fois par jour ou bien une fois par heure, selon le nombre
d’images que vous créez.

_ 2.6 RÉSUMÉ
Les conteneurs d’applications fournissent une bonne abstraction
pour les applications et, lorsqu’ils sont packagés dans des images
au format Docker, les applications deviennent faciles à créer, à
déployer et à distribuer. Les conteneurs offrent aussi une isolation
entre les applications fonctionnant sur le même ordinateur, ce qui
permet d’éviter les conflits de dépendance.
Dans les chapitres suivants, nous allons voir comment la possibilité
de monter des répertoires externes permet non seulement
d’exécuter des applications stateless dans un conteneur, mais aussi
des applications comme mysql ou bien d’autres applications qui
génèrent beaucoup de données.
3

Déploiement d’un cluster


Kubernetes

Maintenant que vous avez réussi à créer un conteneur d’application,


vous êtes motivé pour apprendre à le déployer dans un système
distribué complet, fiable et évolutif. Bien sûr, vous avez besoin pour
cela d’un cluster Kubernetes. Il existe aujourd’hui plusieurs services
Kubernetes basés sur le cloud qui facilitent la création d’un cluster
grâce à quelques instructions en ligne de commande. Nous vous
recommandons fortement cette approche si vous débutez avec
Kubernetes. Même si vous envisagez au bout du compte
d’implémenter Kubernetes sur une machine physique, il est préférable
de démarrer rapidement avec Kubernetes et de compléter sa
formation, pour ensuite apprendre à l’installer sur vos propres
machines.
Bien entendu, l’utilisation d’une solution basée sur le cloud nécessite
de payer pour bénéficier de ces ressources ainsi que d’avoir une
connexion réseau active. C’est pour ces raisons que le
développement en local peut être plus attractif ; si vous êtes dans ce
cas-là, l’outil minikube fournit un moyen simple d’obtenir un cluster
Kubernetes local qui s’exécute sur une VM installée sur votre
ordinateur portable ou un ordinateur de bureau. Bien que cela soit
avantageux financièrement, minikube crée seulement un cluster à
nœud unique, ce qui ne permet pas de démontrer toutes les
possibilités d’un cluster Kubernetes complet. C’est pour cette raison
que nous recommandons de commencer par une solution basée sur
le cloud, à moins que cela ne soit pas possible. Si vous persistez dans
l’idée de démarrer avec votre propre machine, nous détaillons dans
l’Annexe A, à la fin de ce livre, des instructions pour la construction
d’un cluster à partir d’une collection de Raspberry Pi qui sont des
nano-ordinateurs. Ces instructions, qui emploient l’outil kubeadm,
peuvent être adaptées à d’autres machines que le Raspberry Pi.

_ 3.1 INSTALLATION DE KUBERNETES


SUR UN FOURNISSEUR DE CLOUD
PUBLIC
Ce chapitre couvre l’installation de Kubernetes sur les trois principaux
fournisseurs de cloud, Amazon Web Services (AWS), Microsoft Azure
et la plateforme Google Cloud.

3.1.1 Service de conteneurs Google

La plateforme Google Cloud offre un service Kubernetes hébergé


appelé Google Container Engine (GKE). Pour démarrer avec GKE,
vous avez besoin d’un compte Google Cloud Platform avec la
facturation activée et d’avoir installé l’outil gcloud
(https://cloud.google.com/sdk/downloads).
Une fois que gcloud est installé, définissez d’abord une zone par
défaut :
$ gcloud config set compute/zone us-west1-a

Ensuite, vous pouvez créer un cluster :


$ gcloud container clusters create kuar-cluster

Cela prendra quelques minutes. Lorsque le cluster est prêt, vous


pouvez obtenir des informations d’identification sur le cluster avec la
commande :
$ gcloud auth application-default login
À ce stade, vous devriez avoir un cluster configuré et prêt à l’emploi.
Sauf si vous préférez installer Kubernetes ailleurs, vous pouvez aller
directement à la section « Le client Kubernetes »).
Si vous avez des problèmes, les instructions complètes pour créer un
cluster GKE se trouvent dans la documentation Google Cloud
Platform (http://bit.ly/2ver7Po).

3.1.2 Installation de Kubernetes avec le service


de conteneur Azure

Microsoft Azure offre un service Kubernetes hébergé dans le cadre du


service de conteneur Azure. La méthode la plus simple pour démarrer
avec le service de conteneur Azure est d’utiliser le shell intégré Azure
Cloud du portail Azure. Vous pouvez activer l’interpréteur de
commande en cliquant sur l’icône du shell :

dans la barre d’outils située en haut à droite. Le shell bénéficie de


l’outil az automatiquement installé et configuré pour fonctionner avec
votre environnement Azure.
Vous pouvez également installer l’interface de ligne de commande az
sur votre ordinateur local.
Une fois que le shell est installé et fonctionnel, vous pouvez exécuter :
$ az group create --name=kuar --location=westus

Une fois le groupe de ressources créé, vous pouvez créer un cluster à


l’aide de :
$ az acs create --orchestrator-type=kubernetes \
--resource-group=kuar --name=kuar-cluster

Cela prendra quelques minutes. Une fois le cluster créé, vous pouvez
obtenir des informations d’identification sur le cluster avec :
$ az acs kubernetes get-credentials --resource-group=kuar --name=kuar-cluster
Si vous n’avez pas encore installé l’outil kubectl, vous pouvez le faire
en utilisant :
$ az acs kubernetes install-cli

Des instructions complètes pour l’installation de Kubernetes sur Azure


se trouvent dans la documentation Azure (http://bit.ly/2veqXYl).

3.1.3 Installation de Kubernetes sur Amazon


Web Services

AWS n’offre pas actuellement de service hébergé Kubernetes.3 Cette


situation peut évoluer très rapidement car de nouveaux outils
apparaissent régulièrement. Voici deux options qui peuvent faciliter
votre démarrage :
La meilleure façon de lancer un petit cluster pour explorer
Kubernetes à l’aide de cet ouvrage est d’utiliser le produit Quick
Start for Kubernetes by Heptio (http://amzn.to/2veAy1q). Il s’agit
d’un modèle CloudFormation simple qui peut lancer un cluster à
l’aide de la console AWS.
Pour une solution de gestion plus complète, envisagez d’utiliser un
projet appelé kops. Vous trouverez un tutoriel complet sur
l’installation de Kubernetes sur AWS avec kops sur Github
(http://bit.ly/2q86l2n).

_ 3.2 INSTALLATION DE KUBERNETES


EN LOCAL À L’AIDE DE MINIKUBE
Si vous avez besoin d’une expérience de développement en local ou
si vous ne voulez pas payer pour obtenir des ressources dans le
cloud, vous pouvez installer un cluster simple à nœud unique grâce à
minikube. Même si minikube est une bonne simulation d’un cluster
Kubernetes, c’est un produit vraiment destiné au développement en
local, à l’apprentissage et à l’expérimentation. Comme il ne s’exécute
que dans une VM sur un seul nœud, il ne fournit pas la fiabilité d’un
cluster Kubernetes distribué.
En outre, certaines fonctionnalités décrites dans ce livre nécessitent
une intégration avec un fournisseur de cloud et ces dernières ne sont
pas disponibles ou fonctionnent de manière limitée avec minikube.

Vous devez avoir un hyperviseur installé sur votre machine


pour utiliser minikube. Pour les machines sous Linux ou MacOS,
on utilise généralement Virtualbox (https://virtualbox.org). Sous
Windows, l’hyperviseur Hyper-V est l’option par défaut. Assurez-
vous d’installer l’hyperviseur avant d’utiliser minikube.

Vous trouverez minikube sur Github


(https://github.com/kubernetes/minikube). Vous pouvez télécharger
des fichiers binaires pour Linux, MacOS et Windows. Une fois que
minikube est installé, vous pouvez créer un cluster local avec la
commande :
$ minikube start

Cela va créer une VM locale, provisionner Kubernetes, et créer une


configuration locale kubectl qui pointe vers ce cluster.
Lorsque vous en avez terminé avec votre cluster, vous pouvez arrêter
la VM avec :
$ minikube stop

Si vous souhaitez supprimer le cluster, vous pouvez exécuter :


$ minikube delete

_ 3.3 EXÉCUTION DE KUBERNETES


SUR UN RASPBERRY PI
Si vous voulez tester un cluster Kubernetes réaliste, sans devoir
investir trop d’argent, vous pouvez créer un cluster Kubernetes très
convaincant avec des ordinateurs Raspberry Pi pour un coût
relativement faible. Les détails de la construction d’un tel cluster n’ont
pas leur place dans ce chapitre, mais vous les trouverez dans
l’Annexe A, à la fin de ce livre.

_ 3.4 LE CLIENT KUBERNETES


Le client Kubernetes officiel se nomme kubectl ; il s’agit d’un outil en
ligne de commande pour interagir avec l’API Kubernetes. kubectl peut
être utilisé pour gérer la plupart des objets Kubernetes tels que les
pods, les ReplicaSets et les services. kubectl peut également être
utilisé pour explorer et vérifier l’intégrité globale du cluster.
Nous allons utiliser kubectl pour explorer le cluster que vous venez de
créer.

3.4.1 Vérification de l’état du cluster

La première chose que vous pouvez faire est de vérifier la version du


cluster que vous exécutez :
$ version kubectl

Cela affichera deux versions différentes : la version de l’outil kubectl


local, ainsi que la version du serveur API Kubernetes.
Ne vous inquiétez pas si ces versions sont différentes. Les
outils Kubernetes ont une compatibilité ascendante et
descendante avec différentes versions de l’API Kubernetes, tant
que vous utilisez le même numéro de version mineure des outils
et du cluster, et que vous n’essayez pas d’utiliser des
fonctionnalités plus récentes sur un cluster plus ancien.
Kubernetes suit la spécification de version sémantique, le numéro
de version mineure étant le chiffre du milieu (par exemple, 5 pour
la version 1.5.2).

Maintenant que nous sommes assurés que vous pouvez


communiquer avec votre cluster Kubernetes, nous allons approfondir
son exploration.
Tout d’abord, nous pouvons obtenir un diagnostic simple du cluster. Il
s’agit d’un bon moyen de vérifier que votre cluster est en bonne
santé :
$ kubectl get componentstatuses

Le résultat devrait ressembler à ceci :

NAME STATUS MESSAGE ERROR

scheduler Healthy ok

controller-manager Healthy ok

etcd-0 Healthy {"health": "true"}

Comme Kubernetes s’améliore au fil du temps, la sortie de la


commande kubectl évolue également. Ne vous inquiétez pas si
l’affichage n’est pas strictement identique aux exemples listés
dans cet ouvrage.
Vous pouvez voir ici les composants constituant le cluster Kubernetes.
Le controller-manager est responsable de l’exécution de différents
contrôleurs qui régulent le comportement du cluster : par exemple,
s’assurer que tous les réplicas d’un service sont disponibles et en
bonne santé. Le scheduler est responsable du placement de différents
pods sur différents nœuds dans le cluster. Enfin, le serveur ETCD
constitue le stockage du cluster où tous les objets API sont stockés.

3.4.2 Lister les nœuds worker Kubernetes

Ensuite, nous pouvons répertorier tous les nœuds de notre cluster :


$ kubectl get nodes

NAME STATUS AGE VERSION

kubernetes Ready,master 45d v1.7.6

node-1 Ready 45d v1.7.6

node-2 Ready 45d v1.7.6

node-3 Ready 45d v1.7.6

Vous pouvez voir qu’il s’agit d’un cluster à quatre nœuds qui est actif
depuis 45 jours. Dans Kubernetes, les nœuds sont séparés en nœuds
master (conteneurs comme le serveur API, l’ordonnanceur, etc.) qui
gèrent le cluster, et nœuds worker où vos conteneurs seront exécutés.
Kubernetes n’ordonnance généralement pas le travail sur les nœuds
master afin de s’assurer que les charges de travail des utilisateurs ne
nuisent pas au fonctionnement global du cluster.
Vous pouvez utiliser la commande kubectl describe pour obtenir plus
d’informations sur un nœud spécifique tel que node-1 :
$ kubectl describe nodes node-1
Vous voyez d’abord des informations de base sur le nœud :

Name: node-1
Role:

Labels: beta.kubernetes.io/arch=arm

beta.kubernetes.io/os=linux

kubernetes.io/hostname=node-1

Vous pouvez voir que ce nœud exécute le système d’exploitation


Linux et s’exécute sur un processeur ARM.
Ensuite, vous voyez des informations sur le fonctionnement de node-1
lui-même :
Conditions:

Type Status LastHeartbeatTime Reason Message

---- ------ ----------------- ------ -------

Sun, 05 Feb
OutOfDisk False KubeletHasSufficientDisk kubelet…
2017…

Sun, 05 Feb
MemoryPressure False KubeletHasSufficientMemory kubelet…
2017…

Sun, 05 Feb
DiskPressure False KubeletHasNoDiskPressure kubelet…
2017…

Sun, 05 Feb
Ready True KubeletReady kubelet…
2017…

Ces informations montrent que le nœud a suffisamment d’espace


disque et de mémoire, et elles signalent que le nœud master
Kubernetes est sain. Ensuite, il y a des informations sur la capacité de
la machine :
Capacity:

alpha.kubernetes.io/nvidia-gpu: 0

cpu: 4
memory: 882636Ki

pods: 110

Allocatable:

alpha.kubernetes.io/nvidia-gpu: 0

cpu: 4

memory: 882636Ki

pods: 110

Puis on trouve des informations sur le logiciel exécuté sur le nœud,


notamment la version de Docker en cours d’exécution, les versions de
Kubernetes, le noyau Linux, etc. :
System Info:

Machine ID: 9989a26f06984d6dbadc01770f018e3b

System UUID: 9989a26f06984d6dbadc01770f018e3b

Boot ID: 98339c67-7924-446c-92aa-c1bfe5d213e6

Kernel Version: 4.4.39-hypriotos-v7+

OS Image: Raspbian GNU/Linux 8 (jessie)

Operating System: linux

Architecture: arm

Container Runtime Version: docker://1.12.6

Kubelet Version: v1.5.2

Kube-Proxy Version: v1.5.2

PodCIDR: 10.244.2.0/24
ExternalID: node-1

Enfin, il y a des informations sur les pods qui sont actuellement en


cours d’exécution sur ce nœud :
Non-terminated Pods: (3 in total)

CPU CPU Memory Memory


Namespace Name
Requests Limits Requests Limits

--------- ---- ------------ ---------- --------------- -------------

kube- 220Mi
kube-system 260m (6%) 0 (0%) 140Mi (16%)
dns… (25%)

kube-
kube-system 0 (0%) 0 (0%) 0 (0%) 0 (0%)
fla…

kube-
kube-system 0 (0%) 0 (0%) 0 (0%) 0 (0%)
pro…

Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.

CPU Requests CPU Limits Memory Requests Memory Limits

------------ ---------- --------------- -------------

260m (6%) 0 (0%) 140Mi (16%) 220Mi (25%)

No events.

À partir de cette sortie, vous pouvez voir les pods sur le nœud (par
exemple, le pod kube-dns qui fournit des services DNS du cluster), le
CPU et la mémoire que chaque pod demande à partir du nœud, ainsi
que les ressources totales demandées. Il est intéressant de noter ici
que Kubernetes garde à la fois la trace de la demande et de la limite
supérieure des ressources pour chaque pod qui s’exécute sur une
machine. La différence entre les demandes et les limites est décrite
en détail au chapitre 5, mais en un mot, les ressources demandées
par un pod sont garanties sur le nœud, tandis que la limite d’un pod
est le montant maximal d’une ressource donnée qu’un pod peut
consommer. La limite d’un pod peut être supérieure à sa demande, et
dans ce cas, les ressources supplémentaires sont fournies sur le
principe d’une obligation de moyens si bien qu’il n’y a aucune garantie
qu’elles soient présentes sur le nœud.

_ 3.5 COMPOSANTS DU CLUSTER


Un des aspects intéressants de Kubernetes est que de nombreux
composants qui constituent le cluster Kubernetes sont réellement
déployés à l’aide de Kubernetes lui-même. Nous allons examiner
quelques-uns de ces composants qui utilisent un certain nombre de
concepts que nous introduirons dans les chapitres suivants. Tous ces
composants s’exécutent dans l’espace de noms4 kube-system.

3.5.1 Proxy Kubernetes

Le proxy Kubernetes est chargé de router le trafic réseau vers des


services dont la charge est équilibrée dans le cluster Kubernetes.
Pour effectuer son travail, le proxy doit être présent sur chaque nœud
du cluster. Kubernetes a un objet API nommé DaemonSet, que vous
étudierez plus tard dans le livre, et qui est utilisé dans de nombreux
clusters pour assurer les services de proxy. Si votre cluster exécute le
proxy Kubernetes avec un DaemonSet, vous pouvez voir les proxys
avec la commande :
$ kubectl get daemonSets --namespace=kube-system kube-proxy

NAME DESIRED CURRENT READY NODE-SELECTOR AGE

kube-proxy 4 4 4 <none> 45d

3.5.2 DNS Kubernetes

Kubernetes exécute également un serveur DNS, qui fournit un


nommage et une fonctionnalité de découverte pour les services
définis dans le cluster. Ce serveur DNS s’exécute également en tant
que service répliqué sur le cluster. Selon la taille de votre cluster, vous
pouvez voir un ou plusieurs serveurs DNS qui s’exécutent dans votre
cluster. Le service DNS est exécuté en tant que déploiement
Kubernetes, qui gère ces réplicas :

NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE

kube-dns 1 1 1 1 45d

Il existe également un service Kubernetes qui effectue l’équilibrage de


charge pour le serveur DNS :
$ kubectl get services --namespace=kube-system kube-dns

NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE

kube-dns 10.96.0.10 <none> 53/UDP,53/TCP 45d

Cela indique que le service DNS du cluster a l’adresse 10.96.0.10. Si


vous vous connectez à un conteneur dans le cluster, vous verrez que
cette information a été injectée dans le fichier /etc/resolv.conf du
conteneur.

3.5.3 Interface utilisateur de Kubernetes

Le dernier composant Kubernetes est une interface graphique.


L’interface utilisateur est exécutée en tant que réplica unique, mais
elle est toujours gérée par un déploiement Kubernetes pour assurer la
fiabilité et gérer les mises à niveau. Vous pouvez voir ce serveur
d’interface utilisateur avec la commande :
$ kubectl get deployments --namespace=kube-system kubernetes-dashboard

NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE

kubernetes-
1 1 1 1 45d
dashboard
Le tableau de bord dispose également d’un service qui effectue
l’équilibrage de charge du tableau de bord :
$ kubectl get services --namespace=kube-system kubernetes-dashboard

NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE

kubernetes-dashboard 10.99.104.174 <nodes> 80:32551/TCP 45d

Nous pouvons utiliser la command kubectl proxy pour accéder à cette


interface utilisateur. Lancez le proxy Kubernetes avec :
$ kubectl proxy

Cela démarre un serveur fonctionnant sur localhost:8001. Si vous


pointez votre navigateur Web sur http://localhost:8001/ui, vous devriez
voir l’interface utilisateur Web Kubernetes. Vous pouvez utiliser cette
interface pour explorer votre cluster, ainsi que pour créer de nouveaux
conteneurs. Les détails complets de cette interface dépassent la
portée de ce livre, et de plus l’interface change rapidement au fur et à
mesure des améliorations du tableau de bord.

_ 3.6 RÉSUMÉ
Nous espérons à ce stade que vous disposez d’un (voire de trois)
cluster Kubernetes en état de fonctionner et que vous avez utilisé
quelques commandes pour explorer le cluster que vous avez créé.
Par la suite, nous allons passer un peu plus de temps à explorer
l’interface en ligne de commande de ce cluster Kubernetes et vous
apprendrez à maîtriser l’outil kubectl. Dans le reste du livre, vous
utiliserez kubectl et votre cluster de test pour explorer les différents
objets de l’API Kubernetes.
4

Commandes kubectl
courantes

L’utilitaire en ligne de commande kubectl est un outil puissant que


vous utiliserez dans les chapitres suivants pour créer des objets et
interagir avec l’API Kubernetes. Avant cela, il faut apprendre les
commandes de base kubectl qui s’appliquent à tous les objets
Kubernetes.

_ 4.1 ESPACES DE NOMS


Kubernetes utilise les espaces de noms (namespaces) pour
organiser des objets dans le cluster. Vous pouvez vous représenter
un espace de noms comme un dossier qui contient un ensemble
d’objets. Par défaut, kubectl interagit avec l’espace de noms default.
Si vous voulez utiliser un espace de noms différent, vous pouvez
passer à kubectl le paramètre --namespace. Par exemple, kubectl --
namespace=monespace référence des objets de l’espace de noms
monespace.

_ 4.2 CONTEXTES
Si vous souhaitez modifier l’espace de noms par défaut de façon
permanente, vous pouvez utiliser un contexte. Celui-ci est enregistré
dans un fichier de configuration kubectl, qui est généralement situé
dans $HOME/.kube/config. Ce fichier de configuration stocke
également la façon de rechercher votre cluster et de s’y authentifier.
Par exemple, vous pouvez créer un contexte avec un autre espace
de noms par défaut avec la commande kubectl suivante :
$ kubectl config set-context mon-contexte --namespace=monespace

Cela crée un nouveau contexte, mais il n’est pas encore


opérationnel. Pour utiliser ce contexte que vous venez de créer,
vous pouvez exécuter :
$ kubectl config use-context mon-contexte

Les contextes peuvent également être utilisés pour gérer différents


clusters ou différents utilisateurs qui s’y authentifient à l’aide des
paramètres --users ou --clusters de la commande set-context.

_ 4.3 AFFICHAGE D’OBJETS


API KUBERNETES
Tout ce qui se trouve dans Kubernetes est représenté par une
ressource RESTful. Tout au long de ce livre, nous nous référons à
ces ressources en tant qu’objets Kubernetes. Chaque objet
Kubernetes existe à une adresse HTTP unique ; par exemple,
https://your-k8s.com/api/v1/namespaces/default/pods/mon-pod
mène à la représentation d’un pod dans l’espace de noms par défaut
nommé mon-pod. La commande kubectl exécute les requêtes HTTP
vers ces URL pour accéder aux objets Kubernetes qui résident à ces
adresses.
La commande la plus basique pour afficher des objets Kubernetes
via kubectl est get. Si vous exécutez kubectl get <nom-ressource>
vous obtiendrez une liste de toutes les ressources dans l’espace de
noms en cours. Si vous souhaitez obtenir une ressource spécifique,
vous pouvez utiliser kubectl get <nom-ressource> <nom-objet>.
Par défaut, kubectl utilise un format de sortie lisible pour afficher les
réponses à partir du serveur API, mais cet affichage supprime de
nombreux détails des objets pour que chaque objet puisse tenir sur
une ligne de terminal. Si vous voulez obtenir un peu plus
d’informations, vous devez ajouter le paramètre -o wide, qui affiche
plus de détails, sur une ligne plus longue. Si vous souhaitez
visualiser l’objet complet, vous pouvez également afficher les objets
au format JSON brut ou YAML avec les paramètres -o json ou -o
yaml, respectivement.
Il arrive souvent que pour manipuler la sortie de kubectl on supprime
les en-têtes, ce qui est utile quand on combine kubectl avec des
pipes UNIX (par exemple, kubectl... | awk...). Si vous spécifiez le
paramètre --no-headers, kubectl va sauter les en-têtes en haut du
tableau de la sortie.
Une autre tâche courante consiste à extraire des champs
spécifiques de l’objet. kubectl utilise le langage de requête
JSONPath pour sélectionner des champs dans l’objet retourné. Les
détails complets sur JSONPath dépassent la portée de ce chapitre,
mais à titre d’exemple, cette commande va extraire et afficher
l’adresse IP du pod :
$ kubectl get pods mon-pod -o jsonpath --template={.status.podIP}

Si vous êtes intéressé par des informations plus détaillées sur un


objet particulier, utilisez la commande describe :
$ kubectl describe <nom-ressource> <nom-objet>

Cette commande fournit sur plusieurs lignes une description


détaillée et lisible de l’objet ainsi que tous les autres objets et
événements connexes du cluster Kubernetes.
_ 4.4 CRÉATION, MISE À JOUR
ET SUPPRESSION
D’OBJETS KUBERNETES
Les objets de l’API Kubernetes sont représentés sous la forme
fichiers JSON ou YAML. Ces fichiers sont retournés par le serveur
en réponse à une requête ou bien postés sur le serveur sous la
forme d’une demande d’API. Vous pouvez utiliser ces fichiers YAML
ou JSON pour créer, mettre à jour ou supprimer des objets sur le
serveur Kubernetes.
Supposons que vous ayez un objet simple stocké dans obj.yaml.
Vous pouvez utiliser kubectl pour créer cet objet dans Kubernetes en
exécutant :
$ kubectl apply -f obj.yaml

Notez que vous n’avez pas besoin de spécifier le type de ressource


de l’objet car il est obtenu à partir du fichier objet lui-même.
De la même manière, une fois que vous avez apporté des
modifications à l’objet, vous pouvez utiliser à nouveau la commande
apply pour mettre à jour l’objet :
$ kubectl apply -f obj.yaml

Si vous avez envie de faire des modifications interactives,


au lieu de modifier un fichier local, vous pouvez utiliser la
commande edit, qui va télécharger le dernier état de l’objet, puis
lancer un éditeur qui contient la définition :
$ kubectl edit <nom-ressource> <nom-objet>
Après avoir enregistré le fichier, il sera automatiquement
rechargé sur le cluster Kubernetes.
Lorsque vous souhaitez supprimer un objet, vous pouvez
simplement exécuter :
$ kubectl delete -f obj.yaml

Il est cependant important de noter que kubectl ne vous demandera


aucune confirmation de la suppression. Une fois que vous émettez
la commande, l’objet sera supprimé.
De la même manière, vous pouvez supprimer un objet en utilisant
son type de ressource et son nom :
$ kubectl delete <nom-ressource> <nom-objet>

_ 4.5 ÉTIQUETAGE ET ANNOTATION


D’OBJETS
Les étiquettes et les annotations sont des balises pour vos objets.
Nous discuterons des différences entre ces deux notions au
chapitre 6, mais pour l’instant, vous pouvez mettre à jour les
étiquettes et les annotations de tout objet Kubernetes avec les
commandes annotate et label. Par exemple, pour ajouter l’étiquette
color=red à un pod nommé bar, vous pouvez exécuter :
$ kubectl label pods bar color=red

La syntaxe des annotations est identique.


Par défaut, les commandes annotate et label ne vous permettent
pas d’écraser une étiquette existante. Pour ce faire, vous devez
ajouter le paramètre --overwrite.
Si vous souhaitez supprimer une étiquette, vous pouvez utiliser la
syntaxe -<nom-étiquette>- :
$ kubectl label pods bar color-

Cela supprimera l’étiquette color du pod nommé bar.


_ 4.6 COMMANDES DE DÉBOGAGE
kubectl permet également d’exécuter un certain nombre de
commandes utiles pour le débogage de vos conteneurs. Vous
pouvez utiliser ce qui suit pour afficher les journaux d’un conteneur
en cours d’exécution :
$ kubectl logs <nom-pod>

Si vous avez plusieurs conteneurs dans votre pod, vous pouvez


choisir le conteneur à afficher à l’aide du paramètre -c.
Par défaut, kubectl logs répertorie les journaux en cours et s’arrête.
Si vous souhaitez au contraire diffuser en continu les logs vers le
terminal sans quitter, vous pouvez ajouter le paramètre -f (pour
follow, suivre en anglais) à la ligne de commande.
Vous pouvez également utiliser la commande exec pour exécuter
une commande dans un conteneur actif :
$ kubectl exec -it <nom-pod> -- bash

Cela vous fournira un shell interactif à l’intérieur du conteneur en


cours d’exécution afin que vous puissiez effectuer plus de
commandes de débogage.
Enfin, vous pouvez copier des fichiers vers un conteneur et depuis
un conteneur à l’aide de la commande cp :
$ kubectl cp <nom-pod>:/path/to/remote/file /path/to/local/file

Cela copie un fichier d’un conteneur en cours d’exécution vers votre


ordinateur local. Vous pouvez également spécifier des répertoires ou
inverser la syntaxe pour copier un fichier de votre ordinateur local
vers le conteneur.

_ 4.7 RÉSUMÉ
kubectl est un outil puissant pour gérer vos applications dans votre
cluster Kubernetes. Ce chapitre a illustré de nombreuses utilisations
courantes de l’outil, mais kubectl dispose d’une très bonne aide en
ligne que vous pouvez afficher avec la commande :
kubectl help

ou :
kubectl help nom-de-commande
5

Pods

Dans les chapitres précédents, nous avons discuté de la façon dont


vous pouvez vous lancer dans la conteneurisation de votre
application, mais dans le monde réel des déploiements
d’applications en conteneurs, vous aurez souvent envie d’héberger
plusieurs applications sur une seule unité, administrée sur une seule
machine.
Un exemple canonique d’un tel déploiement est illustré à la
figure 5.1 où le système se compose d’un conteneur faisant office de
serveur Web qui répond à des requêtes et d’un conteneur qui
synchronise le système de fichiers avec un dépôt Git distant.

Figure 5.1 – Exemple de pod avec deux conteneurs


et un système de fichiers partagé.
De prime abord, il peut sembler tentant d’encapsuler le serveur Web
et le service de synchronisation avec Git dans un seul conteneur.
Cependant, après un examen plus attentif, les raisons d’une
séparation des deux deviennent claires. Tout d’abord, les deux
conteneurs ont des exigences sensiblement différentes en termes
d’utilisation des ressources. Prenez, par exemple, la mémoire :
comme le serveur Web satisfait les requêtes des utilisateurs, nous
voulons nous assurer qu’il est toujours disponible et réactif. D’autre
part, le service de synchronisation avec Git n’est pas vraiment
orienté utilisateur et n’a qu’une obligation de moyens en matière de
qualité de service.
Supposons que notre synchronisation avec Git ait une fuite de
mémoire. Nous devons nous assurer que la synchronisation ne peut
pas consommer la mémoire que nous voulons utiliser pour notre
serveur Web, car cela pourrait affecter ses performances, voire
planter le serveur.
Ce type de séparation des ressources est exactement le genre de
chose que les conteneurs sont censés accomplir. En séparant les
deux applications en deux conteneurs distincts, nous pouvons
garantir une exploitation fiable du serveur Web.
Bien entendu, les deux conteneurs sont étroitement liés ; il ne serait
pas logique de programmer le serveur Web sur une machine et la
synchronisation Git sur un autre. Par conséquent, Kubernetes
regroupe plusieurs conteneurs en une seule unité atomique appelée
pod (ce nom est lié à la métaphore des baleines des conteneurs
Docker, car un pod désigne aussi un groupe de baleines).

_ 5.1 PODS DANS KUBERNETES


Un pod représente une collection de conteneurs d’applications et de
volumes fonctionnant dans le même environnement d’exécution. Les
pods, et non les conteneurs, sont les plus petits artefacts
déployables dans un cluster Kubernetes. Cela signifie que tous les
conteneurs d’un pod résident toujours sur la même machine.
Chaque conteneur au sein d’un pod s’exécute dans son propre
cgroup, mais les conteneurs partagent un certain nombre d’espaces
de noms Linux.
Les applications s’exécutant dans le même pod partagent la même
adresse IP et le même espace de port (espace de noms réseau), ont
le même nom d’hôte (espace de noms UTS) et peuvent
communiquer à l’aide de canaux de communication interprocessus
natifs sur les files d’attente de messages System V IPC ou POSIX
(espace de noms IPC). Cependant, les applications hébergées dans
des pods différents sont isolées les unes des autres ; elles ont des
adresses IP différentes, des noms d’hôte différents, etc. Les
conteneurs hébergés dans des pods différents fonctionnant sur le
même nœud peuvent aussi être sur des serveurs différents.

_ 5.2 PENSER EN TERMES DE PODS


Quand on commence à adopter Kubernetes, on se pose souvent la
question suivante : « que dois-je mettre dans un pod ? »
Parfois les gens découvrent les pods et pensent qu’ils doivent mettre
dans le même pod un conteneur WordPress et un conteneur de
base de données MySQL. Cependant, ce type de pod est en fait un
bel exemple de modèle à ne pas suivre quand on crée un pod. Il y a
deux raisons à cela. Tout d’abord, WordPress et sa base de
données ne sont pas vraiment étroitement liés. Si le conteneur de
WordPress et le conteneur de base de données sont hébergés sur
des machines différentes, ils peuvent encore fonctionner ensemble
assez efficacement, puisqu’ils communiquent par le biais d’une
connexion réseau. Deuxièmement, vous ne voulez pas
nécessairement faire évoluer WordPress et sa base de données en
tant qu’entité unique. WordPress lui-même est principalement une
application sans état (stateless), et il est par conséquent souhaitable
de faire évoluer vos frontends WordPress en réponse à la charge
frontend en créant plus de pods WordPress. La mise à l’échelle
d’une base de données MySQL est beaucoup plus délicate, et il est
fort probable que vous allez augmenter les ressources dédiées à un
seul pod MySQL. Si vous regroupez les conteneurs WordPress et
MySQL dans un seul pod, vous serez obligé d’utiliser la même
stratégie de mise à l’échelle pour les deux conteneurs, ce qui n’est
pas l’idéal.
En général, la bonne question à se poser lors de la conception des
pods est de savoir si ces conteneurs fonctionneront correctement
s’ils résident sur des machines différentes. Si la réponse est
négative, un pod constitue un regroupement correct pour les
conteneurs. Si la réponse est affirmative, plusieurs pods seront
probablement une bonne solution. Dans l’exemple illustré au début
de ce chapitre, les deux conteneurs interagissent via un système de
fichiers local. Il leur serait donc impossible de fonctionner
correctement si les conteneurs étaient programmés sur des
machines différentes.
Dans les sections suivantes de ce chapitre, nous décrirons comment
créer, analyser, gérer et supprimer des pods dans Kubernetes.

_ 5.3 LE MANIFESTE DE POD


Les pods sont décrits dans un manifeste de pod. Le manifeste de
pod n’est qu’une représentation sous la forme d’un fichier texte d’un
objet API Kubernetes. Kubernetes est fortement axé sur les
configurations déclaratives. Une configuration déclarative signifie
que vous écrivez l’état désiré du monde dans une configuration, puis
que vous soumettez cette configuration à un service qui prend des
mesures pour s’assurer que l’état désiré devient l’état réel.
La configuration déclarative est différente de la
configuration impérative, où il suffit d’exécuter une série
d’instructions (par exemple, apt-get install foo) pour modifier le
monde. Des années d’expérience en production nous ont appris
que la conservation d’un enregistrement écrit de l’état désiré du
système engendre un système plus facile à gérer et plus fiable.
La configuration déclarative a de nombreux avantages,
notamment la révision de code pour les configurations, ainsi que
la documentation de l’état actuel du monde pour les équipes
distribuées. En outre, elle constitue la base de tous les
comportements d’auto-guérison dans Kubernetes qui assurent
le bon fonctionnement des applications sans action de
l’utilisateur.

Le serveur API Kubernetes accepte et traite les manifestes de pods


avant de les stocker dans le stockage persistant (etcd).
L’ordonnanceur utilise également l’API Kubernetes pour rechercher
des pods qui n’ont pas été planifiés sur un nœud. L’ordonnanceur
place ensuite les pods sur les nœuds en fonction des ressources et
d’autres contraintes exprimées dans les manifestes de pods.
Plusieurs pods peuvent être placés sur la même machine tant qu’il y
a suffisamment de ressources. Toutefois, l’ordonnancement de
plusieurs réplicas de la même application sur la même machine
diminue grandement la fiabilité, puisqu’il suffit d’une seule
défaillance pour que la machine s’arrête. Par conséquent,
l’ordonnanceur Kubernetes essaie de s’assurer que les pods de la
même application sont distribués sur des machines différentes pour
augmenter la fiabilité au cas où de telles défaillances se
produiraient. Une fois programmés pour un nœud, les pods ne sont
pas déplacés et ils doivent être explicitement détruits et replanifiés.
Plusieurs instances d’un pod peuvent être déployées en répétant le
workflow décrit ici. Toutefois, les ReplicaSets (chapitre 8)
conviennent mieux pour exécuter plusieurs instances d’un pod (il
s’avère qu’ils sont aussi meilleurs dans la gestion d’un pod unique,
mais nous y reviendrons plus tard).

5.3.1 Création d’un pod

La façon la plus simple de créer un pod est d’exécuter la commande


kubectl. Par exemple, pour exécuter notre serveur kuard, utilisez :
$ kubectl run kuard --image=gcr.io/kuar-demo/kuard-amd64:1

Vous pouvez voir l’état de ce pod en exécutant :


$ kubectl get pods

Il est possible que le conteneur soit d’abord en état d’attente


(Pending), mais au bout d’un certain temps, vous verrez qu’il sera en
cours d’exécution (Running), ce qui signifie que le pod et ses
conteneurs ont été créés avec succès.
Ne vous inquiétez pas trop sur les chaînes de caractères aléatoires
qui figurent à la fin du nom du pod. En créant le pod de cette
manière-là, on utilise les objets Deployment et ReplicaSet que nous
allons étudier dans les chapitres suivants.
Pour l’instant, vous pouvez supprimer ce pod en exécutant :
$ kubectl delete deployments/kuard

Nous allons maintenant passer à la rédaction manuelle d’un


manifeste de pod complet.

5.3.2 Création d’un manifeste de pod

Les manifestes de pods peuvent être écrits au format YAML ou


JSON, mais YAML est généralement le format préféré parce qu’il est
légèrement plus lisible et que l’on a la possibilité d’ajouter des
commentaires. Les manifestes de pods (et les autres objets API
Kubernetes) doivent vraiment être traités de la même manière que le
code source, et les commentaires facilitent la compréhension d’un
pod quand les nouveaux membres de l’équipe l’examinent pour la
première fois.
Les manifestes de pods incluent quelques champs et attributs clés :
principalement une section metadata pour décrire le pod et ses
étiquettes, une section spec pour décrire les volumes, et une liste
des conteneurs qui s’exécuteront dans le pod.
Au chapitre 2, nous avons déployé kuard avec la commande Docker
suivante :
$ docker run -d --name kuard \
--publish 8080:8080 \
gcr.io/kuar-demo/kuard-amd64:1

Un résultat similaire peut être obtenu en écrivant à la place un fichier


au format YAML puis en utilisant les commandes kubectl pour
charger ce manifeste sur Kubernetes.
Voici un exemple du fichier kuard-pod.yaml :
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:1
name: kuard
ports:
- containerPort: 8080
name: http
protocol: TCP

_ 5.4 EXÉCUTION DES PODS


Dans la section précédente, nous avons créé un manifeste de pod
qui peut être utilisé pour démarrer un pod exécutant kuard. Utilisez
la commande kubectl apply pour lancer une instance unique de
kuard :
$ kubectl apply -f kuard-pod.yaml

Le manifeste de pod sera soumis au serveur API Kubernetes. Le


système Kubernetes ordonnancera ensuite ce pod pour qu’il
s’exécute sur un nœud sain dans le cluster, où il sera surveillé par le
processus du démon kubelet. Ne vous inquiétez pas si vous ne
comprenez pas pour l’instant tous les éléments de Kubernetes car
nous fournirons plus de détails tout au long de ce livre.

5.4.1 Liste des pods

Maintenant que nous avons un pod en cours d’exécution, nous


allons en apprendre un peu plus à son sujet. En utilisant l’outil en
ligne de commande kubectl, nous pouvons répertorier toutes les
pods en cours d’exécution dans le cluster. Pour l’instant, cela ne
devrait concerner que le pod unique que nous avons créé à l’étape
précédente :

NAME READY STATUS RESTARTS AGE

kuard 1/1 Running 0 44s

$ kubectl get pods

Vous pouvez voir le nom du pod (kuard) que nous avons donné dans
le fichier de manifeste au format YAML. En plus du nombre de
conteneurs prêts (1/1), la sortie indique également l’état, le nombre
de fois où le pod a été redémarré, ainsi que sa durée d’activité.
Si vous avez exécuté cette commande immédiatement après la
création du pod, vous pouvez voir :

NAME READY STATUS RESTARTS AGE

kuard 0/1 Pending 0 1s

L’état Pending (attente) indique que le pod a été soumis, mais qu’il
n’a pas encore été ordonnancé.
Si une erreur plus importante se produit (par exemple, une tentative
de création d’un pod avec une image conteneur qui n’existe pas),
elle sera également répertoriée dans le champ Status.

Par défaut, l’outil en ligne de commande kubectl essaie


d’être concis dans les informations qu’il affiche, mais vous
pouvez obtenir plus d’informations en ajoutant des paramètres
sur la ligne de commande. En ajoutant -o wide à n’importe
quelle commande kubectl, cela affichera un peu plus
d’informations (tout en essayant de garder les informations sur
une seule ligne). En ajoutant -o json ou -o yaml, cela affichera
les objets complets au format JSON ou YAML, respectivement.

5.4.2 Informations détaillées sur les pods

Parfois, la vue sur une seule ligne est insuffisante parce qu’elle est
trop succincte. En outre, Kubernetes assure la maintenance de
nombreux événements sur les pods qui sont présents dans le flux
d’événements, mais qui ne sont pas attachés à l’objet Pod.
Pour obtenir plus d’informations sur un pod (ou un objet
Kubernetes), vous pouvez utiliser la commande kubectl describe.
Par exemple, pour décrire le pod que nous avons créé
précédemment, vous pouvez exécuter :
$ kubectl describe pods kuard

Cela génère un tas d’informations sur le pod qui sont organisées en


différentes sections. En haut on trouve l’information de base sur le
pod :

Name: kuard

Namespace: default

Node: node1/10.0.15.185
Start Time: Sun, 02 Jul 2017 15:00:38 -0700

Labels: <none>

Annotations: <none>

Status: Running

IP: 192.168.199.238

Controllers: <none>

Ensuite, il y a des informations sur les conteneurs en cours


d’exécution dans le pod :
Containers:
kuard:

Container ID: docker://055095…

Image: gcr.io/kuar-demo/kuard-amd64:1

Image ID: docker-pullable://gcr.io/kuar-demo/kuard-amd64@sha256:a580…

Port: 8080/TCP

State: Running

Started: Sun, 02 Jul 2017 15:00:41 -0700

Ready: True

Restart Count: 0

Environment: <none>

Mounts:

/var/run/secrets/kubernetes.io/serviceaccount from default-token-cg5f5 (ro)


Enfin, il y a des événements liés au pod, comme le moment où il a
été ordonnancé, le moment où son image a été extraite, et s’il a dû
être redémarré en raison de tests d’intégrité présentant des
défaillances :
Events:

Seen From SubObjectPath Type Reason Message

---- ---- ------------- -------- ------ -------

default-
50s Normal Scheduled Success…
scheduler

49s kubelet, node1 spec.containers{kuard} Normal Pulling pulling…

47s kubelet, node1 spec.containers{kuard} Normal Pulled Success…

47s kubelet, node1 spec.containers{kuard} Normal Created Created…

47s kubelet, node1 spec.containers{kuard} Normal Started Started…

5.4.3 Suppression d’un pod

Quand c’est le moment de supprimer un pod, vous pouvez le


supprimer soit en utilisant son nom :
$ kubectl delete pods/kuard

soit en utilisant le même fichier que vous avez employé pour le


créer :
$ kubectl delete -f kuard-pod.yaml

Lorsqu’un pod est supprimé, il n’est pas immédiatement détruit. Au


lieu de cela, si vous exécutez kubectl get pods, vous verrez que le
pod est dans l’état Terminating. Tous les pods ont un délai pour
s’arrêter qui, par défaut, est fixé à 30 secondes. Lorsqu’un pod est
dans l’état Terminating, il ne reçoit plus de nouvelles requêtes. Dans
un scénario de service, le délai est important pour la fiabilité parce
qu’il permet au pod de terminer toutes les requêtes actives qu’il peut
être en train de traiter avant qu’il ne s’arrête.
Il est important de noter que lorsque vous supprimez un pod, toutes
les données stockées dans les conteneurs associés à ce pod seront
également supprimées. Si vous souhaitez conserver des données
sur plusieurs instances d’un pod, vous devez utiliser des volumes
persistants qui sont évoqués à la fin de ce chapitre.

_ 5.5 ACCÈS À VOTRE POD


Maintenant que votre pod est en cours d’exécution, ce ne sont pas
les raisons qui manquent de vouloir y accéder. Vous souhaiterez
peut-être charger le service Web qui s’exécute dans le pod. Vous
souhaiterez peut-être afficher ses journaux pour déboguer un
problème que vous constatez, ou même exécuter d’autres
commandes dans le contexte du pod pour faciliter le débogage. Les
sections suivantes décrivent les différentes façons dont vous pouvez
interagir avec le code et les données qui s’exécutent dans votre pod.

5.5.1 Utilisation de la redirection de port

Plus loin dans le livre, nous allons vous montrer comment exposer
un service au monde ou à d’autres conteneurs à l’aide d’équilibreurs
de charge, mais la plupart du temps vous souhaiterez simplement
accéder à un pod spécifique, même s’il ne gère aucun trafic sur
Internet.
Pour ce faire, vous pouvez utiliser la prise en charge de la
redirection de port intégrée à l’API Kubernetes et disponible avec les
outils en ligne de commande.
Lorsque vous exécutez :
$ kubectl port-forward kuard 8080:8080
un tunnel sécurisé est créé à partir de votre ordinateur local, via le
master Kubernetes, vers l’instance du pod s’exécutant sur l’un des
nœuds worker.
Tant que la commande de redirection de port est en cours
d’exécution, vous pouvez accéder au pod (en l’occurrence, avec
l’interface Web de kuard) sur http://localhost:8080.

5.5.2 Obtenir plus d’informations avec les journaux

Lorsque votre application a besoin de débogage, il est utile de


pouvoir obtenir plus d’informations détaillées que celles disponibles
avec la commande describe afin de comprendre ce que fait
l’application. Kubernetes fournit deux commandes pour le débogage
des conteneurs en cours d’exécution. La commande kubectl logs
télécharge les journaux de l’instance en cours d’exécution :
$ kubectl logs kuard

Si vous ajoutez à la commande le paramètre -f, vous obtiendrez un


flux continu des journaux.
La commande kubectl logs essaie toujours d’obtenir des journaux à
partir du conteneur en cours d’exécution. Si vous ajoutez à la
commande le paramètre --previous, vous obtiendrez les journaux
d’une instance précédente du conteneur. Cela s’avère utile, par
exemple, si vos conteneurs redémarrent continuellement en raison
d’un problème au démarrage du conteneur.
Même si l’emploi de kubectl logs est utile pour le
débogage ponctuel des conteneurs dans les environnements de
production, on se sert généralement d’un service d’agrégation
de journaux. Il existe plusieurs outils d’agrégation de journaux
open source, comme fluentd et elasticsearch, ainsi que de
nombreux fournisseurs de journalisation dans le cloud. Les
services d’agrégation de journaux fournissent une plus grande
capacité de stockage des journaux sur une plus longue durée,
ainsi que de riches fonctionnalités de recherche et de filtrage
des journaux. Enfin, ils fournissent souvent la possibilité
d’agréger les journaux de plusieurs pods en une seule vue.

5.5.3 Exécution de commandes dans votre conteneur


avec exec

Parfois, les journaux sont insuffisants, et pour vraiment déterminer


ce qui se passe, vous devez exécuter des commandes dans le
contexte du conteneur lui-même. Pour ce faire, vous pouvez utiliser :
$ kubectl exec kuard date

Vous pouvez également obtenir une session interactive en ajoutant


les paramètres -it :
$ kubectl exec -it kuard ash

5.5.4 Copie de fichiers entre des conteneurs

Il vous arrivera parfois de devoir copier des fichiers d’un conteneur


distant vers un ordinateur local en vue d’une exploration plus
approfondie. Par exemple, vous pouvez utiliser un outil comme
Wireshark pour visualiser les captures de paquets tcpdump.
Supposons que vous ayez un fichier nommé /captures/capture3.txt
dans un conteneur de votre pod. Vous pouvez copier ce fichier en
toute sécurité sur votre ordinateur local en exécutant :
$ kubectl cp <nom-du-pod>:/captures/capture3.txt ./capture3.txt

D’autres fois, il vous faudra peut-être copier des fichiers de votre


machine locale dans un conteneur. Admettons que vous vouliez
copier $HOME/config.txt sur un conteneur distant. Dans ce cas,
vous pouvez exécuter :
$ kubectl cp $HOME/config.txt <nom-du-pod>:/config.txt

En général, la copie de fichiers dans un conteneur n’est pas une


pratique recommandée. Vous devez vraiment traiter le contenu d’un
conteneur comme étant une structure immutable. Mais parfois, c’est
le moyen le plus rapide pour stopper une hémorragie et remettre en
marche un service ; en effet, ce traitement d’urgence est plus rapide
que la construction, l’envoi, et le déploiement d’une nouvelle image.
Cependant, une fois que le problème est jugulé, il est vraiment
important que vous reconstruisiez immédiatement l’image pour la
déployer tout de suite, sinon vous pouvez être certain d’oublier la
modification que vous avez faite en local sur votre conteneur si bien
que vous allez l’écraser lors du prochain déploiement qui aura été
normalement ordonnancé.

_ 5.6 CONTRÔLES D’INTÉGRITÉ


Lorsque vous exécutez votre application sous la forme d’un
conteneur dans Kubernetes, elle est automatiquement surveillée
grâce à une vérification de l’intégrité des processus. Ce contrôle
d’intégrité garantit simplement que le processus principal de votre
application est toujours en cours d’exécution. Si ce n’est pas le cas,
Kubernetes le redémarre.
Toutefois, dans la plupart des cas, un simple contrôle des processus
est insuffisant. Par exemple, si votre processus est bloqué et n’est
pas en mesure de répondre aux requêtes, un contrôle d’intégrité du
processus va croire que votre application est encore en bonne santé
puisque son processus est toujours en cours d’exécution.
Pour y remédier, Kubernetes a introduit des contrôles d’intégrité de
l’activité des applications. Les vérifications d’intégrité de l’activité
exécutent une logique spécifique à l’application (par exemple, le
chargement d’une page Web) pour vérifier que l’application n’est pas
simplement en cours d’exécution, mais qu’elle fonctionne
correctement. Étant donné que ces vérifications de l’activité sont
spécifiques à l’application, vous devez les définir dans votre
manifeste de pod.

5.6.1 Sonde d’activité

Une fois que le processus kuard est en cours d’exécution, nous


avons besoin d’un moyen de confirmer qu’il est réellement en bonne
santé et qu’il ne doit pas être redémarré. Les sondes d’activité sont
définies par conteneur, ce qui signifie que chaque conteneur à
l’intérieur d’un pod est vérifié séparément. Dans l’exemple 5-2, nous
ajoutons une sonde d’activité à notre conteneur kuard, qui exécute
une requête HTTP sur le chemin /healthy de notre conteneur.
Exemple 5-2. kuard-pod-health.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:1
name: kuard
livenessProbe:
httpGet:
path: /healthy
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 1
periodSeconds: 10
failureThreshold: 3
ports:
- containerPort: 8080
name: http
protocol: TCP

Le manifeste de pod précédent utilise une sonde httpGet pour


effectuer une requête HTTP GET sur le port 8080 du point de
terminaison /healthy du conteneur kuard. La sonde définit un
initialDelaySeconds de 5 secondes, si bien qu’elle ne sera appelée
qu’après un délai de cinq secondes une fois que tous les conteneurs
du pod auront été créés. La sonde doit répondre dans le délai
d’attente d’une seconde, et le code d’état HTTP doit être égal ou
supérieur à 200 et inférieur à 400 pour être considéré comme un
succès. Kubernetes appellera la sonde toutes les 10 secondes. Si
plus de trois sondes sont en échec, le conteneur considère qu’il est
en panne et redémarre.
Vous pouvez constater cela en regardant la page d’état de kuard.
Créez un pod à l’aide de ce manifeste, puis faites une redirection de
port sur ce pod :
$ kubectl apply -f kuard-pod-health.yaml
$ kubectl port-forward kuard 8080:8080

Pointez votre navigateur sur http://localhost:8080. Cliquez sur


l’onglet « Liveness probe ». Vous devriez voir un tableau qui
répertorie toutes les sondes que cette instance de kuard a reçues. Si
vous cliquez sur le lien « fail » de cette page, kuard va commencer à
échouer aux contrôles d’intégrité. Attendez suffisamment longtemps
et Kubernetes redémarrera le conteneur. À ce moment, l’affichage
se réinitialisera et recommencera. Les détails du redémarrage
peuvent être visualisés grâce à la commande kubectl describe pods
kuard. La section « Events » affichera un texte similaire à ce qui
suit :
Killing container with id docker://2ac946...:pod "kuard_default(9ee84...)"
container "kuard" is unhealthy, it will be killed and re-created.

5.6.2 Sonde de disponibilité


Bien entendu, l’activité n’est pas le seul type de contrôle d’intégrité
que nous souhaitons effectuer. Kubernetes fait une distinction entre
l’activité (liveness) et la disponibilité (readiness). L’activité détermine
si une application fonctionne correctement et les conteneurs qui
échouent aux vérifications d’activité sont redémarrés. L’état de
disponibilité décrit si un conteneur est prêt à répondre aux requêtes
des utilisateurs. Les conteneurs qui échouent aux vérifications de
disponibilité sont supprimés des équilibreurs de charge des services.
Les sondes de disponibilité sont configurées de la même manière
que les sondes d’activité. Nous explorerons les services Kubernetes
en détail auchapitre 7.
Le fait de combiner les sondes de disponibilité et d’activité permet de
s’assurer que seuls les conteneurs sains s’exécutent dans le cluster.

5.6.3 Types de contrôles d’intégrité

En plus des contrôles HTTP, Kubernetes prend également en charge


les contrôles d’intégrité tcpSocket qui ouvrent un socket TCP ; si la
connexion est réussie, la sonde considère que c’est un succès. Ce
style de sonde est utile pour les applications qui n’emploient pas
HTTP, par exemple, les bases de données ou d’autres API qui ne
sont pas basées sur HTTP.
Enfin, Kubernetes autorise les sondes exec. Ces dernières
exécutent un script ou un programme dans le contexte du conteneur
et suivent la convention typique : si le script renvoie un code de
sortie égal à zéro, alors la sonde réussit ; dans le cas contraire, elle
échoue. Les scripts exec sont souvent utiles pour les logiques de
validation d’application personnalisée qui ne correspondent pas
parfaitement au modèle d’appel HTTP.

_ 5.7 GESTION DES RESSOURCES


La plupart des gens migrent vers les conteneurs et les
orchestrateurs comme Kubernetes en raison des améliorations
importantes qu’ils procurent dans le packaging des images et la
fiabilité de leur déploiement. En plus des primitives orientées
application qui simplifient le développement des systèmes distribués,
tout aussi importante est la capacité d’augmenter l’utilisation globale
des nœuds de calcul qui composent le cluster. Le coût de base de
l’exploitation d’une machine, qu’elle soit virtuelle ou physique, est
fondamentalement constant et ne dépend pas du fait qu’elle soit
inactive ou qu’elle travaille à plein régime. Par conséquent, veiller à
ce que ces machines aient une activité maximale augmente la
rentabilité de chaque euro dépensé pour l’infrastructure.
En règle générale, nous mesurons cette efficacité grâce à une
métrique d’utilisation. L’utilisation est définie comme la quantité
d’une ressource activement utilisée divisée par le montant d’une
ressource qui a été achetée. Par exemple, si vous achetez un
ordinateur mono-cœur et que votre application utilise un dixième du
cœur, votre utilisation est de 10 %.
Avec des systèmes d’ordonnancement comme Kubernetes qui gère
le packaging des ressources, vous pouvez atteindre un taux
d’utilisation de plus de 50%.
Pour ce faire, vous devez lister à Kubernetes les ressources dont
votre application a besoin, de telle sorte que Kubernetes puisse
trouver le packaging optimal des conteneurs sur les machines
achetées.
Kubernetes permet aux utilisateurs de spécifier deux métriques de
ressources différentes. Les requêtes de ressources spécifient la
quantité minimale de ressource nécessaire à l’exécution de
l’application. Les limites de ressources spécifient la quantité
maximale de ressource qu’une application peut consommer. Ces
deux définitions de ressources sont décrites plus en détail dans les
sections suivantes.

5.7.1 Requêtes de ressources : ressources minimales


requises

Avec Kubernetes, un pod demande les ressources nécessaires à


l’exécution de ses conteneurs. Kubernetes garantit que ces
ressources sont disponibles pour le pod. Les ressources les plus
couramment demandées sont le CPU et la mémoire, mais
Kubernetes prend en compte d’autres types de ressources comme
le GPU.
Par exemple, pour demander que le conteneur kuard soit hébergé
sur une machine avec 50 % du CPU disponible et 128 Mo de
mémoire allouée, on définit le pod de la manière suivante :
Exemple 5-3. kuard-pod-resreq.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:1
name: kuard
resources:
requests:
cpu: "500m"
memory: "128Mi"
ports:
- containerPort: 8080
name: http
protocol: TCP
Les ressources sont demandées par conteneur, et non
par pod. Le total des ressources demandées par le pod est la
somme de toutes les ressources demandées par l’ensemble
des conteneurs du pod. On procède de la sorte car dans de
nombreux cas les différents conteneurs ont des besoins très
différents en matière de CPU. Par exemple, dans le pod où
résident un serveur Web et un système de synchronisation de
données, le serveur Web est orienté vers l’utilisateur et a
probablement besoin de beaucoup de ressources CPU, tandis
que la synchronisation de données exige très peu de
ressources.

5.7.2 Détails sur les limites des requêtes

Les requêtes sont utilisées lors de l’ordonnancement des pods sur


des nœuds. L’ordonnanceur Kubernetes s’assure que la somme de
toutes les requêtes de l’ensemble des pods sur un nœud ne
dépasse pas la capacité du nœud. Par conséquent, Kubernetes
garantit qu’un pod aura au moins les ressources demandées lors de
l’exécution sur le nœud. Il est important de noter que la requête
spécifie un minimum. Elle ne spécifie pas de plafond maximal de
ressources qu’un pod peut utiliser. Pour comprendre ce que cela
signifie, examinons un exemple.
Imaginez que nous ayons un conteneur dont le code tente d’utiliser
tous les cœurs CPU disponibles. Supposons que l’on crée un pod
avec ce conteneur qui demande 0,5 CPU. Kubernetes ordonnance
ce pod sur une machine avec un total de deux cœurs CPU.
Tant qu’il est le seul pod sur la machine, il consommera la totalité
des deux cœurs disponibles, même s’il n’a demandé que 0,5 CPU.
Si un deuxième pod avec le même conteneur et la même demande
de 0,5 CPU est hébergé sur la machine, alors chaque pod disposera
d’un cœur.
Si un troisième pod identique est ordonnancé, chaque pod recevra
0,66 CPU. Enfin, si un quatrième pod identique est ordonnancé,
chaque pod recevra 0,5 CPU qu’il a demandé, et le nœud aura
atteint sa capacité maximale.
Les requêtes CPU sont implémentées à l’aide de la fonctionnalité
cpu-shares du noyau Linux.

Les requêtes de mémoire sont traitées de la même


manière que celles du CPU, mais il y a une différence
importante. Si un conteneur dépasse sa demande de mémoire,
le système d’exploitation ne peut pas simplement supprimer de
la mémoire du processus, car elle a été allouée. Par
conséquent, lorsque le système est à court de mémoire, le
kubelet termine les conteneurs dont l’utilisation de la mémoire
est supérieure à la mémoire demandée. Ces conteneurs sont
automatiquement redémarrés, mais ils disposent de moins de
mémoire disponible sur la machine.

Comme les requêtes de ressources garantissent la disponibilité des


ressources pour un pod, elles sont essentielles pour s’assurer que
les conteneurs disposent de ressources suffisantes dans des
situations de charge élevée.

5.7.3 Plafonnement de l’utilisation des ressources


avec des limites

En plus de la définition des ressources requises par un pod, qui


établit les ressources minimales disponibles pour le pod, vous
pouvez aussi définir un seuil maximal pour l’utilisation des
ressources d’un pod via les limites de ressources.
Dans notre exemple précédent, nous avons créé un pod kuard qui
demandait un minimum de 0,5 CPU et 128 Mo de mémoire. Dans le
manifeste de pod de l’exemple 5-4, nous étendons cette
configuration pour ajouter une limite de 1 CPU et 256 Mo de
mémoire.
Exemple 5-4. kuard-pod-reslim.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:1
name: kuard
resources:
requests:
cpu: "500m"
memory: "128Mi"
limits:
cpu: "1000m"
memory: "256Mi"
ports:
- containerPort: 8080
name: http
protocol: TCP

Lorsque vous établissez des limites sur un conteneur, le noyau est


configuré pour garantir que la consommation ne puisse pas
dépasser ces limites. Un conteneur avec une limite CPU de
0,5 cœur n’obtiendra jamais plus de 0,5 cœur, même si le CPU est
par ailleurs inactif. Un conteneur avec une limite mémoire de 256 Mo
ne sera pas autorisé à obtenir plus de mémoire (par exemple, malloc
générera un message d’erreur) s’il utilise plus de 256 Mo.

_ 5.8 PERSISTANCE DES DONNÉES AVEC


DES VOLUMES
Lorsqu’un pod est supprimé ou qu’un conteneur redémarre, toutes
les données du système de fichiers du conteneur sont également
supprimées. C’est souvent une bonne chose, car il n’est pas
souhaitable de laisser des fichiers inutiles produits par votre
application Web stateless. En revanche, dans d’autres cas, il est
important de conserver un accès permanent au disque, ce qui
garantit la bonne santé d’une application. Kubernetes modélise un
tel stockage persistant.

5.8.1 Utilisation de volumes avec des pods

Pour ajouter un volume à un manifeste de pod, il suffit d’ajouter deux


nouvelles sections à notre configuration. La première est une
nouvelle section spec.volumes. Ce tableau définit dans le manifeste
tous les volumes auxquels les conteneurs peuvent accéder. Il est
important de noter que tous les conteneurs ne sont pas nécessaires
pour monter tous les volumes définis dans le pod. Le deuxième ajout
est le tableau volumeMounts dans la définition de conteneur. Ce
tableau définit les volumes qui sont montés dans un conteneur
particulier, et le chemin d’accès où chaque volume doit être monté.
Vous noterez que deux conteneurs différents dans un pod peuvent
monter le même volume dans différents chemins de montage.
Le manifeste de l’exemple 5-5 définit un nouveau volume unique
nommé kuard-data, que le conteneur kuard monte dans le chemin
/data.
Exemple 5-5. kuard-pod-vol.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
volumes:
- name: "kuard-data"
hostPath:
path: "/var/lib/kuard"
containers:
- image: gcr.io/kuar-demo/kuard-amd64:1
name: kuard
volumeMounts:
- mountPath: "/data"
name: "kuard-data"
ports:
- containerPort: 8080
name: http
protocol: TCP

5.8.2 Différentes façons d’utiliser des volumes avec


des pods

Vous pouvez utiliser des données dans votre application de


différentes façons. Vous trouverez ci-dessous quelques-uns des
modèles recommandés pour Kubernetes.

◆ Communication/synchronisation

Dans le premier exemple de pod, nous avons vu comment deux


conteneurs utilisaient un volume partagé pour un serveur Web tout
en le gardant synchronisé avec un dépôt Git distant. Pour y parvenir,
le pod utilise un volume emptyDir. Un tel volume dépend de la durée
de vie du pod, mais il peut être partagé entre deux conteneurs,
formant ainsi la base de la communication entre le conteneur de
synchronisation Git et le conteneur de serveur Web.

◆ Cache

Une application peut utiliser un volume qui est utile pour améliorer
les performances, mais qui n’est pas nécessaire au bon
fonctionnement de l’application. Par exemple, une application
conserve peut-être des vignettes d’images plus volumineuses. Bien
entendu, ces vignettes peuvent être reconstituées à partir des
images originales, mais leur affichage sera alors moins performant.
Si vous voulez qu’un tel cache ne soit pas supprimé après un
redémarrage du conteneur dû à un contrôle défaillant, emptyDir
fonctionne également bien dans ce cas de figure.

◆ Données persistantes

Parfois, vous utiliserez un volume pour des données réellement


persistantes, c’est-à-dire des données qui sont indépendantes de la
durée de vie d’un pod particulier, et que vous devrez déplacer entre
les nœuds du cluster si un nœud est défaillant ou si un pod est
déplacé vers un autre ordinateur pour une raison quelconque. Pour
ce faire, Kubernetes prend en charge une grande variété de volumes
de stockage réseau distant, notamment les protocoles largement
répandus comme NFS ou iSCSI ainsi que le stockage réseau des
fournisseurs de cloud comme Elastic Block Store d’Amazon, le
stockage de disque d’Azure, ainsi que le système de disque
persistant de Google.

◆ Montage du système de fichiers hôte

D’autres applications n’ont pas réellement besoin d’un volume


persistant, mais elles nécessitent un accès au système de fichiers
hôte sous-jacent. Par exemple, elles peuvent avoir besoin d’accéder
au système de fichiers /dev afin d’effectuer un accès de bas niveau
des blocs d’un périphérique sur le réseau. Pour ces cas-là,
Kubernetes prend en charge le volume hostPath, qui peut monter
des emplacements arbitraires sur le nœud worker du conteneur.
L’exemple précédent utilise le type de volume hostPath. Le volume
créé est /var/lib/kuard sur l’hôte.

5.8.3 Persistance des données à l’aide de disques


distants

Souvent, vous voulez que les données qu’un pod utilise demeurent
dans ce pod, même s’il est redémarré sur une machine hôte
différente.
Pour ce faire, vous pouvez monter un volume de stockage réseau
distant dans votre pod. Lors de l’utilisation du stockage basé sur le
réseau, Kubernetes monte et démonte automatiquement le stockage
approprié chaque fois qu’un pod utilisant ce volume est ordonnancé
sur une machine particulière.
Il existe de nombreuses méthodes pour monter des volumes sur le
réseau. Kubernetes inclut la prise en charge des protocoles standard
tels que NFS et iSCSI, ainsi que les API de stockage basées sur les
principaux fournisseurs de cloud (publics et privés). Dans de
nombreux cas, les fournisseurs de cloud créent également le disque
pour vous s’il n’existe pas déjà.
Voici un exemple d’utilisation d’un serveur NFS :
...
# Rest of pod definition above here
volumes:
- name: "kuard-data"
nfs:
server: my.nfs.server.local
path: "/exports"

_ 5.9 SYNTHÈSE
De nombreuses applications sont stateful, et en tant que telles, nous
devons conserver toutes les données et garantir l’accès au volume
de stockage sous-jacent, quelle que soit la machine sur laquelle
l’application s’exécute. Comme nous l’avons vu précédemment, cela
peut être réalisé à l’aide d’un volume persistant sauvegardé par le
stockage connecté au réseau. Nous voulons également garantir
qu’une instance saine de l’application est en cours d’exécution en
permanence, ce qui signifie que nous voulons nous assurer que le
conteneur qui exécute kuard est prêt avant de l’exposer aux clients.
Grâce à une combinaison de volumes persistants, de sondes de
disponibilité et d’activité, et de restrictions de ressources,
Kubernetes fournit tout ce qu’il faut pour exécuter les applications
stateful de manière fiable. L’exemple 5-6 rassemble tout cela dans
un seul manifeste.
Exemple 5-6. kuard-pod-full.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
volumes:
- name: "kuard-data"
nfs:
server: my.nfs.server.local
path: "/exports"
containers:
- image: gcr.io/kuar-demo/kuard-amd64:1
name: kuard
ports:
- containerPort: 8080
name: http
protocol: TCP
resources:
requests:
cpu: "500m"
memory: "128Mi"
limits:
cpu: "1000m"
memory: "256Mi"
volumeMounts:
- mountPath: "/data"
name: "kuard-data"
livenessProbe:
httpGet:
path: /healthy
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 1
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 30
timeoutSeconds: 1
periodSeconds: 10
failureThreshold: 3

Les volumes persistants sont un vaste sujet qui présente de


nombreux aspects différents, en particulier la manière dont les
volumes persistants, les demandes de volume persistants et le
provisionnement dynamique du volume fonctionnent ensemble. Ce
sujet sera traité de manière plus approfondie au chapitre 13.

_ 5.10 RÉSUMÉ
Les pods représentent l’unité atomique de travail dans un cluster
Kubernetes. Les pods sont composés d’un ou de plusieurs
conteneurs travaillant en symbiose. Pour créer un pod, on écrit un
manifeste de pod et on le soumet au serveur API Kubernetes à l’aide
de l’outil en ligne de commande ou (moins fréquemment) en
effectuant des appels HTTP et JSON directement sur le serveur.
Une fois que vous avez soumis le manifeste au serveur API,
l’ordonnanceur Kubernetes trouve une machine où le pod peut être
hébergé et il le planifie sur cette machine. Une fois ordonnancé, le
démon kubelet de cette machine est responsable de la création des
conteneurs qui correspondent au pod, ainsi que de l’exécution de
tous les contrôles d’intégrité définis dans le manifeste.
Une fois qu’un pod est ordonnancé pour un nœud, aucune
replanification n’a lieu en cas de défaillance de ce nœud. De plus,
pour créer plusieurs réplicas du même pod, vous devez les créer et
les nommer manuellement. Dans un prochain chapitre, nous
étudierons l’objet ReplicaSet et nous montrerons comment
automatiser la création de plusieurs pods identiques et garantir qu’ils
sont recréés en cas de défaillance de la machine du nœud.
6

Étiquettes et annotations

Kubernetes a été conçu pour accompagner votre croissance, au fur


et à mesure que votre application grandit à la fois en taille et en
complexité. C’est avec cette idée en tête que les étiquettes et les
annotations ont été ajoutées en tant que concepts fondateurs. Les
étiquettes et les annotations vous permettent de travailler avec des
ensembles de choses qui correspondent à la façon dont vous
pensez votre application. Vous pouvez organiser, marquer et indexer
toutes vos ressources pour représenter les groupes qui font sens
pour votre application.
Les étiquettes sont des paires clé/valeur qui peuvent être attachées
à des objets Kubernetes tels que des pods et des ReplicaSets. Elles
peuvent être arbitraires, et sont utiles pour associer des informations
d’identification aux objets Kubernetes. Les étiquettes fournissent la
base pour le regroupement des objets.
Les annotations, quant à elles, fournissent un mécanisme de
stockage qui ressemble aux étiquettes : les annotations sont des
paires clé/valeur conçues pour contenir des informations qui ne sont
pas identifiantes, mais qui peuvent être exploitées par des outils et
des bibliothèques.

_ 6.1 ÉTIQUETTES
Les étiquettes fournissent des métadonnées d’identification pour les
objets. Elles représentent les qualités fondamentales de l’objet qui
seront utilisées pour leur regroupement, leur affichage et leur
exploitation.

L’origine des étiquettes vient de l’expérience de Google


dans la gestion d’applications volumineuses et complexes.
Quelques leçons ont été tirées de cette expérience et vous
pouvez consulter l’excellent ouvrage Site Reliability Engineering
de Niall Murphy, Betsy Beyer, Chris Jones, Jennifer Petoff
(publié chez O’Reilly) pour des informations approfondies sur la
façon dont Google aborde les systèmes de production.

La première leçon est que la production déteste les exemplaires


uniques. Lorsque vous déployez des logiciels, les utilisateurs
commencent souvent avec une seule instance. Cependant, à
mesure que l’application mûrit, ces exemplaires uniques se
multiplient souvent et deviennent des ensembles d’objets. Dans
cet esprit, Kubernetes utilise des étiquettes pour traiter des
ensembles d’objets plutôt que des instances uniques.
La deuxième leçon est que toute hiérarchie imposée par le
système sera insuffisante pour de nombreux utilisateurs. En
outre, le regroupement et la hiérarchie des utilisateurs sont au fil
du temps sujets à des modifications. Par exemple, un utilisateur
peut commencer avec l’idée que toutes les applications sont
composées de nombreux services. Toutefois, au fil du temps, un
service peut être partagé entre plusieurs applications. Les
étiquettes Kubernetes sont suffisamment flexibles pour
s’adapter à ces situations.

Les étiquettes ont une syntaxe simple. Il s’agit de paires clé/valeur


où la clé et la valeur sont représentées par des chaînes de
caractères. Les clés d’étiquettes peuvent être décomposées en deux
parties : un préfixe optionnel et un nom, séparés par un slash. Le
préfixe, s’il est spécifié, doit être un sous-domaine DNS limité à
253 caractères. Le nom de la clé est obligatoire et doit avoir moins
de 63 caractères. Les noms, qui doivent commencer et se terminer
par un caractère alphanumérique, autorisent l’utilisation de tirets (-),
de barre de soulignement (_) et de points (.) entre les caractères.
Les valeurs d’étiquettes sont des chaînes d’une longueur maximale
de 63 caractères. Le contenu des valeurs d’étiquettes suit les
mêmes règles que pour les clés d’étiquettes.
Le tableau 6-1 affiche des clés et des valeurs d’étiquettes valides.

Tableau 6.1 – Exemples d’étiquettes

Clé Valeur

acme.com/app-version 1.0.0

appVersion 1.0.0

app.version 1.0.0

kubernetes.io/cluster-
true
service

6.1.1 Création des étiquettes

Dans cet exemple, nous allons créer quelques déploiements (ce qui
illustrera la manière de créer une matrice de pods) avec quelques
étiquettes intéressantes. Nous allons prendre deux applications
(appelées alpaca et bandicoot) qui auront chacune leur
environnement. Nous aurons aussi deux versions différentes.
1. Tout d’abord, on crée le déploiement alpaca-prod et on définit les
étiquettes ver, app et env :
$ kubectl run alpaca-prod \
--image=gcr.io/kuar-demo/kuard-amd64:1 \
--replicas=2 \
--labels="ver=1,app=alpaca,env=prod"

2. Ensuite, on crée le déploiement alpaca-test et on définit les


étiquettes ver, app et env avec les valeurs appropriées :
$ kubectl run alpaca-test \
--image=gcr.io/kuar-demo/kuard-amd64:2 \
--replicas=1 \
--labels="ver=2,app=alpaca,env=test"

3. Enfin, on crée deux déploiements pour bandicoot. Ici on nomme


les environnements prod et staging :
$ kubectl run bandicoot-prod \
--image=gcr.io/kuar-demo/kuard-amd64:2 \
--replicas=2 \
--labels="ver=2,app=bandicoot,env=prod"
$ kubectl run bandicoot-staging \
--image=gcr.io/kuar-demo/kuard-amd64:2 \
--replicas=1 \
--labels="ver=2,app=bandicoot,env=staging"

À ce stade, vous devriez avoir quatre déploiements : alpaca-prod,


alpaca-test, bandicoot-prod, et bandicoot-staging.
$ kubectl get deployments --show-labels

NAME ... LABELS

alpaca-prod ... app=alpaca,env=prod,ver=1

alpaca-test ... app=alpaca,env=test,ver=2

bandicoot-prod ... app=bandicoot,env=prod,ver=2

bandicoot-staging ... app=bandicoot,env=staging,ver=2

Nous pouvons visualiser cela sous la forme d’un diagramme de


Venn basé sur les étiquettes (figure 6-1).
Figure 6.1 – Visualisation des étiquettes appliquées
à nos déploiements.

6.1.2 Modification des étiquettes

Les étiquettes peuvent être mises à jour après leur création.


$ kubectl label deployments alpaca-test "canary=true"

Il faut ici faire une mise en garde importante. Dans cet


exemple, la commande kubectl label ne changera l’étiquette que
sur le déploiement lui-même ; cela n’affectera pas les objets
(ReplicaSets et pods) créés par le déploiement. Pour changer
ces objets, vous devez modifier le modèle incorporé dans le
déploiement (voir le chapitre 12).

Vous pouvez aussi utiliser l’option -L de kubectl get pour afficher une
valeur d’étiquette sous la forme d’une colonne :
$ kubectl get deployments -L canary

NAME DESIRED CURRENT ... CANARY

alpaca-prod 2 2 <none>

alpaca-test 1 1 true
bandicoot-prod 2 2 <none>

bandicoot-staging 1 1 <none>

Vous pouvez supprimer une étiquette en appliquant un tiret comme


suffixe :
$ kubectl label deployments alpaca-test "canary-"

6.1.3 Sélecteurs d’étiquettes

Les sélecteurs d’étiquettes sont utilisés pour filtrer les objets


Kubernetes en fonction d’un ensemble d’étiquettes. Les sélecteurs
utilisent un langage booléen simple. Ils sont utilisés à la fois par les
utilisateurs finaux (via des outils comme kubectl) et par différents
types d’objets (par exemple, pour les relations entre un ReplicaSet
et ses pods).
Chaque déploiement (via un ReplicaSet) crée un jeu de pods à l’aide
des étiquettes spécifiées dans le modèle incorporé dans le
déploiement. Cela est configuré par la commande kubectl run.
La commande kubectl get pods doit retourner tous les pods en cours
d’exécution dans le cluster. Nous devons avoir un total de six pods
kuard dans nos trois environnements :
$ kubectl get pods --show-labels

NAME ... LABELS

alpaca-prod-3408831585-4nzfb ... app=alpaca,env=prod,ver=1,...

alpaca-prod-3408831585-kga0a ... app=alpaca,env=prod,ver=1,...

alpaca-test-1004512375-3r1m5 ... app=alpaca,env=test,ver=2,...

bandicoot-prod-373860099-0t1gp ... app=bandicoot,env=prod,ver=2,...

bandicoot-prod-373860099-k2wcf ... app=bandicoot,env=prod,ver=2,...


bandicoot-staging-1839769971-3ndv ... app=bandicoot,env=staging,ver=2,...

Vous pouvez voir une nouvelle étiquette que nous


n’avons pas encore rencontrée : pod-template-hash. Cette
étiquette est appliquée par le déploiement de manière à pouvoir
garder une trace pour savoir quels pods ont été générés à partir
de quelles versions du modèle. Cela permet au déploiement de
gérer les mises à jour de manière propre, comme cela sera
étudié en détail au chapitre 12.

Si nous voulons seulement lister les pods qui ont l’étiquette ver
définie à 2, nous pouvons utiliser le paramètre --selector :
$ kubectl get pods --selector="ver=2"

NAME READY STATUS RESTARTS AGE

alpaca-test-1004512375-3r1m5 1/1 Running 0 3m

bandicoot-prod-373860099-0t1gp 1/1 Running 0 3m

bandicoot-prod-373860099-k2wcf 1/1 Running 0 3m

bandicoot-staging-1839769971-3ndv5 1/1 Running 0 3m

Si nous spécifions deux sélecteurs séparés par une virgule, seuls les
objets qui satisfont les deux conditions seront retournés. Il s’agit
d’une opération avec un ET logique :
$ kubectl get pods --selector="app=bandicoot,ver=2"

NAME READY STATUS RESTARTS AGE

bandicoot-prod-373860099-0t1gp 1/1 Running 0 4m


bandicoot-prod-373860099-k2wcf 1/1 Running 0 4m

bandicoot-staging-1839769971-3ndv5 1/1 Running 0 4m

Nous pouvons aussi demander si une étiquette appartient à un


ensemble de valeurs. Ici, nous demandons d’afficher tous les pods
où l’étiquette app est définie à alpaca ou bandicoot (cette commande
va retourner les six pods) :
$ kubectl get pods --selector="app in (alpaca,bandicoot)"

NAME READY STATUS RESTARTS AGE

alpaca-prod-3408831585-4nzfb 1/1 Running 0 6m

alpaca-prod-3408831585-kga0a 1/1 Running 0 6m

alpaca-test-1004512375-3r1m5 1/1 Running 0 6m

bandicoot-prod-373860099-0t1gp 1/1 Running 0 6m

bandicoot-prod-373860099-k2wcf 1/1 Running 0 6m

bandicoot-staging-1839769971-3ndv5 1/1 Running 0 6m

Enfin, nous pouvons demander si une étiquette est définie. Ici, nous
demandons tous les déploiements ayant l’étiquette canary définie
avec n’importe quelle valeur :
$ kubectl get deployments --selector="canary"

NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE

alpaca-test 1 1 1 1 7m

Il y a aussi des versions « négatives » de chacun de ces opérateurs,


comme l’indique le tableau 6-2.
Tableau 6.2 – Opérateurs
des sélecteurs

Opérateur Description

clé=valeur clé définie à valeur

clé non définie à


clé!=valeur
valeur

clé in (valeur1, clé égale à valeur1


valeur2) ou valeur2

clé notin (valeur1, clé non égale à


valeur2) valeur1 ou valeur2

clé clé définie

!clé clé non définie

6.1.4 Sélecteurs d’étiquettes dans les objets API

Quand un objet Kubernetes fait référence à un ensemble d’autres


objets Kubernetes, un sélecteur d’étiquettes est utilisé. Au lieu d’une
chaîne simple comme nous l’avons décrit dans la section
précédente, une structure analysée est utilisée.
Pour des raisons historiques (Kubernetes ne rompt pas la
compatibilité API !), il existe deux formes. La plupart des objets
prennent en charge un ensemble plus récent et plus puissant
d’opérateurs de sélecteurs.
Par exemple, le sélecteur app=alpaca, ver in (1, 2) sera converti en :
selector:
matchLabels:
app: alpaca
matchExpressions:
- {key: ver, operator: In, values: [1, 2]}5

Tous les termes sont évalués en tant que ET logique. La seule façon
de représenter l’opérateur != est de le convertir en une expression
NotIn avec une valeur unique.
L’ancienne forme de spécification des sélecteurs (utilisée dans les
ReplicationControllers et les services) ne prend en charge que
l’opérateur =. Il s’agit d’un ensemble simple de paires clé/valeur qui
doivent toutes correspondre à un objet cible à sélectionner.
Le sélecteur app=alpaca, ver=1 sera représenté de la manière
suivante :
selector:
app: alpaca
ver: 1

_ 6.2 ANNOTATIONS
Les annotations fournissent un emplacement pour stocker des
métadonnées supplémentaires pour les objets Kubernetes dans le
seul but de simplifier la tâche des outils et des bibliothèques. Elles
offrent la possibilité à d’autres programmes de piloter Kubernetes via
une API pour stocker des données opaques dans un objet. Les
annotations peuvent être utilisées pour l’outil lui-même ou pour
transmettre des informations de configuration entre des systèmes
externes.
Alors que les étiquettes sont utilisées pour identifier et regrouper des
objets, les annotations sont employées pour fournir des informations
supplémentaires sur l’emplacement d’un objet, son utilisation ou sa
stratégie. Ces deux fonctionnalités se recouvrent, et l’opportunité
d’utiliser une annotation ou une étiquette est une affaire de goût. En
cas de doute, ajoutez des informations à un objet sous la forme
d’une annotation et transformez-la en étiquette si vous voulez
l’utiliser dans un sélecteur.
Les annotations sont utilisées pour :
Garder une trace des motifs de la dernière mise à jour d’un objet.
Communiquer une politique d’ordonnancement spécialisée à un
planificateur spécialisé.
Compléter les données sur le dernier outil employé pour mettre à
jour la ressource et documenter la manière dont la ressource a
été mise à jour (utilisé pour détecter les modifications par d’autres
outils et effectuer une fusion intelligente).
Les informations concernant le build, la release ou l’image qui ne
peuvent pas être gérées avec des étiquettes (cela peut inclure un
hachage Git, un timestamp, un numéro de PR, etc.).
Activer l’objet Deployment (chapitre 12) pour garder une trace des
ReplicaSets qu’il gère pour les déploiements.
Fournir des données supplémentaires pour améliorer la qualité
visuelle ou la convivialité d’une interface utilisateur. Par exemple,
les objets peuvent inclure un lien vers une icône (ou une version
codée en base64 d’une icône).
Prototyper une fonctionnalité alpha dans Kubernetes (au lieu de
créer un champ API de première classe, les paramètres de cette
fonctionnalité sont encodés dans une annotation).
Les annotations sont utilisées en divers endroits dans Kubernetes,
mais principalement dans les déploiements. Pendant les
déploiements, les annotations sont utilisées pour suivre l’état du
déploiement et fournir les informations nécessaires à une
restauration dans un état antérieur.
Les utilisateurs doivent éviter d’utiliser le serveur API Kubernetes
comme une base de données à usage général. Les annotations sont
conçues pour les petites quantités de données qui sont associées à
une ressource spécifique. Si vous souhaitez stocker des données
dans Kubernetes mais que vous n’avez pas d’objet évident à y
associer, envisagez de stocker ces données dans une autre base de
données plus appropriée.

6.2.1 Création des annotations


Les clés des annotations utilisent le même format que les clés
d’étiquettes. Toutefois, comme elles sont souvent utilisées pour
communiquer des informations entre les outils, la partie « espace de
noms » de la clé est plus importante. Par exemple, les clés peuvent
inclure deployment.kubernetes.io/revision ou kubernetes.io/change-
cause.
Le composant valeur d’une annotation est un champ caractère dont
le format est libre. Alors que cela autorise une flexibilité maximale
puisque les utilisateurs peuvent stocker des données arbitraires, il
n’y a donc pas de validation du format en raison de ce caractère
arbitraire. Par exemple, il n’est pas rare qu’un document JSON soit
encodé sous la forme d’une chaîne de caractères et soit stocké dans
une annotation. Il est important de noter que le serveur Kubernetes
n’a aucune connaissance du format exigé pour les valeurs des
annotations. Si les annotations sont utilisées pour passer ou stocker
des données, il n’y a aucune garantie que les données soient
valides. Cela peut rendre la détection des erreurs plus difficile.
Les annotations sont définies dans la section metadata de chaque
objet Kubernetes :
...
metadata:
annotations:
example.com/icon-url: "https://example.com/icon.png"
...

Les annotations sont très pratiques et fournissent une liaison à la


fois souple et puissante. Toutefois, elles doivent être utilisées
judicieusement pour éviter un désordre de données non typées.

_ 6.3 NETTOYAGE
Il est facile de nettoyer tous les déploiements que nous avons
expérimentés dans ce chapitre :
$ kubectl delete deployments --all

Si vous voulez être plus sélectif, vous pouvez utiliser le paramètre --


selector pour choisir les déploiements à supprimer.

_ 6.4 RÉSUMÉ
Les étiquettes sont utilisées pour identifier et éventuellement
regrouper des objets dans un cluster Kubernetes. Les étiquettes
sont aussi utilisées dans les requêtes de sélecteur pour fournir un
regroupement flexible d’objets en cours d’exécution tels que des
pods.
Les annotations offrent un stockage des métadonnées des objets
sous la forme clé/valeur qui peuvent être utilisées par des outils
d’automatisation et des bibliothèques clientes. Les annotations
peuvent aussi être utilisées pour stocker des données de
configuration pour des outils externes tels que des ordonnanceurs
tiers et des outils de surveillance.
Les étiquettes et les annotations sont essentielles pour comprendre
comment les composants clés d’un cluster Kubernetes fonctionnent
ensemble afin de garantir l’état du cluster désiré. L’utilisation
correcte des étiquettes et des annotations est la clé de la flexibilité
de Kubernetes et fournit le point de départ pour la construction
d’outils d’automatisation et de workflows de déploiement.
7

Découverte des services

Kubernetes est un système très dynamique. Le système gère le placement des


pods sur les nœuds, en veillant à ce qu’ils soient en état de fonctionner, et les
replanifie si besoin. Il existe des moyens de modifier automatiquement le nombre
de pods en fonction de la charge (comme la mise à l’échelle automatique
(autoscaling) horizontale des pods [voir la section « Mise à l’échelle automatique
d’un ReplicaSet » du chapitre 8]). La nature pilotée par l’API du système encourage
la création de niveaux de plus en plus élevés d’automatisation.
Alors que la nature dynamique de Kubernetes facilite l’exécution de nombreuses
choses, cela crée des problèmes quand il s’agit de trouver ces ressources. La
plupart des infrastructures réseau traditionnelles n’ont pas été conçues pour le
niveau de dynamisme offert par Kubernetes.

_ 7.1 QU’EST-CE QUE LA DÉCOUVERTE


DES SERVICES ?
Le nom générique pour cette classe de problèmes est la découverte des services
(service discovery). Les outils de la découverte des services aident à résoudre le
problème de savoir quels processus écoutent à quelles adresses quels services.
Un bon système de découverte des services permet aux utilisateurs de trouver ces
informations rapidement et de manière fiable. Un bon système a aussi peu de
latence et les clients sont mis à jour peu après que l’information associée à un
service est modifiée. Enfin, un bon système de découverte des services peut
stocker une définition plus riche du service en question, par exemple s’il y a
plusieurs ports associés au service.
Le système de noms de domaine (DNS) est le système traditionnel de découverte
des services sur Internet. Le DNS est conçu pour une résolution de nom
relativement stable avec une mise en cache efficace. C’est un système parfait pour
Internet, mais il s’avère insuffisant dans le monde dynamique de Kubernetes.
Malheureusement, de nombreux systèmes (par exemple, Java, par défaut)
recherchent un nom directement dans le DNS et ne refont jamais de tentative de
résolution. Cela pourrait en effet amener les clients à mettre en cache des
mappages périmés si bien qu’ils dialogueraient avec la mauvaise adresse IP.
Même avec des délais TTL courts et des clients qui fonctionnent bien, il y a un
délai normal entre un changement de résolution de nom et le moment où le client
en est averti. Il existe aussi des limites à la quantité et au type d’information que
peut retourner une requête DNS classique. Les choses commencent à se gâter
après 20 à 30 enregistrements A pour un nom unique. Les enregistrements SRV
règlent certains problèmes, mais ils sont souvent très difficiles à utiliser. Enfin, la
façon dont les clients gèrent plusieurs adresses IP dans un enregistrement DNS
est en général de prendre la première adresse IP et de compter sur le serveur DNS
pour établir l’ordre des enregistrements de façon aléatoire ou bien d’utiliser un
mécanisme de round-robin. Ce système ne remplace donc pas un équilibrage de
charge sur mesure.

_ 7.2 L’OBJET SERVICE


La véritable découverte des services dans Kubernetes commence par un objet
Service.
Un objet Service est un moyen de créer un sélecteur d’étiquette nommé. Comme
nous le verrons, l’objet Service réalise aussi d’autres prouesses.
Tout comme la commande kubectl run est un moyen facile de créer un déploiement
Kubernetes, on peut utiliser kubectl expose pour créer un service. Pour voir
comment les choses fonctionnent, nous allons créer des déploiements et des
services :
$ kubectl run alpaca-prod \
--image=gcr.io/kuar-demo/kuard-amd64:1 \
--replicas=3 \
--port=8080 \
--labels="ver=1,app=alpaca,env=prod"
$ kubectl expose deployment alpaca-prod
$ kubectl run bandicoot-prod \
--image=gcr.io/kuar-demo/kuard-amd64:2 \
--replicas=2 \
--port=8080 \
--labels="ver=2,app=bandicoot,env=prod"
$ kubectl expose deployment bandicoot-prod
$ kubectl get services -o wide

NAME CLUSTER-IP ... PORT(S) ... SELECTOR


alpaca-prod 10.115.245.13 ... 8080/TCP ... app=alpaca,env=prod,ver=1

bandicoot-prod 10.115.242.3 ... 8080/TCP ... app=bandicoot,env=prod,ver=2

kubernetes 10.115.240.1 ... 443/TCP ... <none>

Après avoir exécuté ces commandes, nous avons trois services. Ceux que nous
venons de créer sont alpaca-prod et bandicoot-prod. Le service kubernetes est
créé automatiquement afin que vous puissiez trouver l’API Kubernetes et
communiquer avec elle à partir de l’application.
Si l’on regarde la colonne SELECTOR, on peut voir que le service alpaca-prod
donne simplement un nom à un sélecteur et spécifie les ports avec lesquels il faut
dialoguer pour ce service. La commande kubectl expose extrait facilement le
sélecteur d’étiquettes et les ports correspondants (8080, en l’occurrence) à partir
de la définition du déploiement.
En outre, un nouveau type d’adresse IP virtuelle, appelé IP de cluster, est assigné
à ce service. Il s’agit d’une adresse IP spéciale pour laquelle le système réalise un
équilibrage de charge à travers tous les pods identifiés par le sélecteur.
Pour interagir avec les services, nous allons faire une redirection de port sur l’un
des pods alpaca. Exécutez la commande suivante dans une fenêtre de terminal
(vous pourrez voir le fonctionnement de la redirection de port en accédant au pod
alpaca à http://localhost:48858) :
$ ALPACA_POD=$(kubectl get pods -l app=alpaca \
-o jsonpath='{.items[0].metadata.name}')
$ kubectl port-forward $ALPACA_POD 48858:8080

7.2.1 Service DNS

Comme l’IP de cluster est virtuelle, elle est stable et il convient de lui donner une
adresse DNS. Cela met fin à tous les problèmes des clients qui mettent en cache
les résultats DNS. Dans un espace de noms, c’est aussi facile que d’utiliser le nom
du service pour se connecter à l’un des pods identifiés par un service.
Kubernetes fournit un service DNS qui est exposé aux pods en cours d’exécution
dans le cluster. Ce service DNS Kubernetes a été installé en tant que composant
système lorsque le cluster a été créé pour la première fois. Le service DNS est lui-
même géré par Kubernetes, et cela représente un excellent exemple de
l’architecture Kubernetes bâtie sur Kubernetes. Le service DNS Kubernetes fournit
des noms DNS pour les adresses IP du cluster.
Vous pouvez le tester en développant la section « DNS Query » de la page d’état
du serveur kuard. Faites une requête sur l’enregistrement A du service alpaca-
prod. La sortie doit ressembler à quelque chose comme ce qui suit :
;; opcode: QUERY, status: NOERROR, id: 12071
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;alpaca-prod.default.svc.cluster.local. IN A

;; ANSWER SECTION:
alpaca-prod.default.svc.cluster.local. 30 IN A 10.115.245.13

Le nom DNS complet est ici alpaca-prod.default.svc.cluster.local.. Décomposons-


le.
alpaca-prod
C’est le nom du service en question.
default
C’est l’espace de noms dans lequel ce service se trouve.
svc
Cela permet de reconnaître qu’il s’agit d’un service. Ainsi Kubernetes aura la
possibilité d’exposer d’autres types de choses que le DNS à l’avenir.
cluster.local.
C’est le nom de domaine de base du cluster. Il s’agit de la valeur par défaut et c’est
ce que vous verrez dans la plupart des clusters. Les administrateurs peuvent
changer cela pour autoriser des noms DNS uniques sur plusieurs clusters.
Lorsque vous faites référence à un service dans votre propre espace de noms,
vous pouvez simplement utiliser le nom du service (alpaca-prod). Vous pouvez
aussi faire référence à un service dans un autre espace de noms avec alpaca-
prod.default. Et, bien sûr, vous pouvez utiliser le nom complet du service (alpaca-
prod.default.svc.cluster.local.). Essayez chacune de ces possibilités dans la section
« DNS Query » de kuard.

7.2.2 Vérification de la disponibilité

Souvent, quand une application démarre pour la première fois, elle n’est pas prête
à traiter les requêtes. Il y a généralement un certain temps d’initialisation qui peut
aller d’une durée inférieure à la seconde jusqu’à plusieurs minutes. L’objet Service
a la bonne idée de surveiller les pods qui sont prêts grâce à un contrôle de leur
disponibilité. Nous allons modifier notre déploiement pour ajouter une vérification
de la disponibilité :
$ kubectl edit deployment/alpaca-prod

Cette commande va récupérer la version actuelle du déploiement alpaca-prod et la


charger dans un éditeur. Après avoir enregistré les modifications et quitté l’éditeur,
l’objet sera retransféré à Kubernetes. Il s’agit d’un moyen rapide de modifier un
objet sans l’enregistrer dans un fichier YAML.
Ajoutez la section suivante :
spec:
...
template:
...
spec:
containers:
...
name: alpaca-prod
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 2
initialDelaySeconds: 0
failureThreshold: 3
successThreshold: 1

Cela définit les pods que ce déploiement va créer et vérifie leur disponibilité via un
HTTP GET sur /ready via le port 8080. Cette vérification est effectuée toutes les
2 secondes dès la création du pod. Si trois contrôles successifs échouent, alors le
pod sera considéré comme n’étant pas prêt. Toutefois, si un seul contrôle réussit, le
pod sera de nouveau considéré comme étant prêt.
On n’envoie du trafic que sur les pods prêts.
La mise à jour de la définition de déploiement telle que nous venons de la voir va
supprimer et recréer les pods alpaca. En tant que tel, nous devons redémarrer la
commande de redirection de port décrite plus haut :
$ ALPACA_POD=$(kubectl get pods -l app=alpaca \
-o jsonpath='{.items[0].metadata.name}')
$ kubectl port-forward $ALPACA_POD 48858:8080

En pointant votre navigateur à http://localhost:48858, vous devriez voir la page de


débogage de cette instance de kuard. Développez la section « Readiness Probe ».
Vous devriez voir cette page mise à jour chaque fois qu’il y a une nouvelle
vérification de disponibilité du système, ce qui devrait se produire toutes les
2 secondes.
Dans une autre fenêtre de terminal, démarrez une commande watch sur les points
de terminaison du service alpaca-prod. Les points de terminaison sont une
méthode de plus bas niveau pour déterminer le trafic qu’un service envoie et ils
seront étudiés plus loin dans ce chapitre. L’option --watch provoque l’attente de la
commande kubectl qui va afficher toutes les mises à jour. Il s’agit d’un moyen
simple de voir comment un objet Kubernetes évolue au fil du temps :
$ kubectl get endpoints alpaca-prod --watch
Retournez à présent à votre navigateur et cliquez sur le lien « Fail » pour vérifier la
disponibilité. Vous devriez voir que le serveur retourne à présent 500 s. Après trois
échecs, ce serveur est supprimé de la liste des points de terminaison du service.
Cliquez sur le lien « Succeed » et vous constaterez qu’après une seule vérification
de la disponibilité, le point de terminaison est à nouveau ajouté.
Cette vérification de la disponibilité est un moyen pour un serveur surchargé ou en
mauvais état de signaler au système qu’il ne veut plus recevoir de trafic. C’est un
excellent moyen de mettre en œuvre l’arrêt approprié. Le serveur peut signaler qu’il
ne veut plus recevoir de trafic, attendre que les connexions existantes soient
fermées, puis s’arrêter proprement.
Appuyez sur CTRL-C pour sortir des commandes port-forward et watch dans vos
terminaux.

_ 7.3 DÉPASSER LES LIMITES DU CLUSTER


Jusqu’à présent, tout ce que nous avons étudié dans ce chapitre concernait
l’exposition des services à l’intérieur d’un cluster. Souvent, les adresses IP des
pods ne sont en effet accessibles qu’à partir du cluster, mais à un moment donné,
nous devons envisager d’accepter du trafic entrant.
La manière la plus facile d’assurer cela est d’utiliser une fonction appelée NodePort
qui améliore les services. En plus d’une adresse IP de cluster, le système
sélectionne un port (mais l’utilisateur peut aussi en spécifier un), et chaque nœud
du cluster transfère le trafic sur ce port pour exposer le service.
Avec cette fonctionnalité, si vous pouvez atteindre un nœud du cluster, vous
pouvez contacter un service. On utilise NodePort sans savoir où les pods de ce
service s’exécutent. Cela peut être intégré aux équilibreurs de charge matériels ou
logiciels pour exposer le service.
Essayez ceci en modifiant le service alpaca-prod :
$ kubectl edit service alpaca-prod

Changez le champ spec.type en NodePort. Vous pouvez aussi réaliser cela lors de
la création du service via kubectl expose en spécifiant --type=NodePort. Le
système attribuera un nouveau NodePort :
$ kubectl describe service alpaca-prod

Name: alpaca-prod
Namespace: default
Labels: app=alpaca
env=prod
ver=1
Annotations: <none>
Selector: app=alpaca,env=prod,ver=1
Type: NodePort
IP: 10.115.245.13
Port: <unset> 8080/TCP
NodePort: <unset> 32711/TCP
Endpoints: 10.112.1.66:8080,10.112.2.104:8080,10.112.2.105:8080
Session Affinity: None
No events.

Ici, nous voyons que le système a assigné le port 32711 à ce service. Maintenant,
nous pouvons atteindre n’importe quel nœud de notre cluster sur ce port afin
d’accéder au service. Si vous vous trouvez sur le même réseau, vous pouvez y
accéder directement. Si votre cluster est quelque part dans le cloud, vous pouvez
utiliser un tunnel SSH avec une commande du genre :
$ ssh <node> -L 8080:localhost:32711

Maintenant, si vous pointez votre navigateur à http://localhost:8080 vous serez


connecté à ce service. Chaque requête que vous envoyez au service sera dirigée
de façon aléatoire vers l’un des pods qui implémentent le service. Rechargez la
page plusieurs fois et vous verrez que vous êtes assigné à différents pods de
manière aléatoire.
Lorsque vous avez terminé, quittez la session SSH.

_ 7.4 INTÉGRATION AU CLOUD


Enfin, si cette fonctionnalité est prise en charge par le service de cloud que vous
employez (et si votre cluster est configuré pour en tirer parti), vous pouvez utiliser
le type LoadBalancer. Cela ajoute NodePort à la configuration du cloud pour créer
un nouvel équilibreur de charge et le diriger sur les nœuds de votre cluster.
Modifiez à nouveau le service alpaca-prod (kubectl edit service alpaca-prod) et
changez spec.type en LoadBalancer.
Si vous faites juste après un kubectl get services, vous verrez que la colonne
EXTERNAL-IP pour alpaca-prod affiche à présent <pending>. Attendez un peu et
vous devriez voir une adresse publique attribuée par votre cloud. Vous pouvez
regarder dans la console de votre compte de cloud et voir le travail de configuration
que Kubernetes a réalisé à votre place :
$ kubectl describe service alpaca-prod

Name: alpaca-prod
Namespace: default

Labels: app=alpaca

env=prod

ver=1

Selector: app=alpaca,env=prod,ver=1

Type: LoadBalancer

IP: 10.115.245.13

LoadBalancer Ingress: 104.196.248.204

Port: <unset> 8080/TCP

NodePort: <unset> 32711/TCP

Endpoints: 10.112.1.66:8080,10.112.2.104:8080,10.112.2.105:8080

Session Affinity: None

Events:

FirstSeen ... Reason Message

--------- ... ------ -------

3m ... Type NodePort -> LoadBalancer

3m ... CreatedLoadBalancer Creating load balancer

2m ... CreatingLoadBalancer Created load balancer

Ici, nous voyons que nous avons une adresse 104.196.248.204 qui est maintenant
assignée au service alpaca-prod. Ouvrez votre navigateur et essayez-la !
Cet exemple provient d’un cluster géré sur la plateforme Google Cloud via GKE.
Vous noterez cependant que la façon dont le LoadBalancer est configuré est
spécifique au service de cloud. En outre, certains services de cloud ont des
équilibreurs de charge basés sur le DNS (par exemple, AWS ELB). Dans ce cas,
vous verrez un nom d’hôte à la place d’une adresse IP. En fonction du fournisseur
de cloud, cela peut aussi prendre un peu de temps pour que l’équilibreur de charge
soit pleinement opérationnel.
_ 7.5 FONCTIONNALITÉS AVANCÉES
Kubernetes est conçu pour être un système extensible. En tant que tel, il existe des
couches qui permettent des intégrations plus avancées. Si vous comprenez les
détails de la façon dont un concept sophistiqué comme les services est mis en
œuvre, cela facilitera le dépannage ou vous aidera à créer des intégrations plus
poussées. Cette section va approfondir certains détails.

7.5.1 Points de terminaison

Certaines applications (et le système lui-même) veulent être en mesure d’utiliser


des services sans employer d’adresse IP de cluster. Cela se fait avec un autre type
d’objet d’appelé Endpoints (points de terminaison). Pour chaque objet Service,
Kubernetes crée un objet Endpoints compagnon qui contient les adresses IP de ce
service :
$ kubectl describe endpoints alpaca-prod

Name: alpaca-prod

Namespace: default

Labels: app=alpaca

env=prod

ver=1

Subsets:

Addresses: 10.112.1.54,10.112.2.84,10.112.2.85

NotReadyAddresses: <none>

Ports:

Name Port Protocol

---- ---- --------

<unset> 8080 TCP

No events.
Pour utiliser un service, une application avancée peut communiquer directement
avec l’API Kubernetes afin de rechercher des points de terminaison et les appeler.
L’API Kubernetes a même la capacité de « surveiller » les objets et d’être avertie
dès qu’ils sont modifiés. De cette façon, un client peut réagir immédiatement dès
que les adresses IP associées à un service changent.
Nous allons vous montrer comment cela fonctionne. Dans une fenêtre de terminal,
exécutez la commande suivante et laissez-la tourner :
$ kubectl get endpoints alpaca-prod --watch

Cela va afficher l’état actuel du point de terminaison, et puis attendre :

NAME ENDPOINTS AGE

alpaca-prod 10.112.1.54:8080,10.112.2.84:8080,10.112.2.85:8080 1m

Ouvrez à présent une autre fenêtre de terminal et supprimez puis recréez le


déploiement alpaca-prod :
$ kubectl delete deployment alpaca-prod
$ kubectl run alpaca-prod \
--image=gcr.io/kuar-demo/kuard-amd64:1 \
--replicas=3 \
--port=8080 \
--labels="ver=1,app=alpaca,env=prod"

Si vous revenez sur le terminal affichant la sortie du point de terminaison qui est
surveillé, vous verrez que lorsque vous avez supprimé et recréé ces pods, la sortie
de la commande reflète les adresses IP mises à jour qui sont associées au service.
Votre sortie ressemblera à quelque chose de ce genre :

NAME ENDPOINTS AGE

alpaca-prod 10.112.1.54:8080,10.112.2.84:8080,10.112.2.85:8080 1m

alpaca-prod 10.112.1.54:8080,10.112.2.84:8080 1m

alpaca-prod <none> 1m

alpaca-prod 10.112.2.90:8080 1m

alpaca-prod 10.112.1.57:8080,10.112.2.90:8080 1m

alpaca-prod 10.112.0.28:8080,10.112.1.57:8080,10.112.2.90:8080 1m

L’objet Endpoints est parfait si vous écrivez un nouveau code qui est créé pour
s’exécuter sur Kubernetes depuis le début. Malheureusement, la plupart des
projets ne sont pas conçus dans ce cadre-là ! La plupart des systèmes existants
sont construits pour fonctionner avec de vieilles adresses IP classiques qui ne
changent pas aussi souvent.

7.5.2 Découverte manuelle des services

Les services Kubernetes sont construits au-dessus des sélecteurs d’étiquettes sur
des pods. Cela signifie que vous pouvez utiliser l’API Kubernetes pour effectuer
une découverte des services rudimentaire sans utiliser aucun objet Service,
comme nous allons vous en faire la démonstration.
Avec kubectl (et via l’API), nous pouvons facilement voir quelles adresses IP sont
attribuées à chaque pod dans notre exemple de déploiement :
$ kubectl get pods -o wide --show-labels

NAME ... IP ... LABELS

alpaca-prod-12334-
... 10.112.1.54 ... app=alpaca,env=prod,ver=1
87f8h

alpaca-prod-12334-
... 10.112.2.84 ... app=alpaca,env=prod,ver=1
jssmh

alpaca-prod-12334-tjp56 ... 10.112.2.85 ... app=alpaca,env=prod,ver=1

bandicoot-prod-5678-
... 10.112.1.55 ... app=bandicoot,env=prod,ver=2
sbxzl

bandicoot-prod-5678-
... 10.112.2.86 ... app=bandicoot,env=prod,ver=2
x0dh8

C’est parfait, mais que se passe-t-il si vous avez de très nombreux pods ? Vous
souhaiterez probablement filtrer cela en fonction des étiquettes appliquées dans le
cadre du déploiement. Faisons cela uniquement pour l’application alpaca :
$ kubectl get pods -o wide --selector=app=alpaca,env=prod

NAME ... ... IP ...

alpaca-prod-3408831585-bpzdz ... 10.112.1.54 ...

alpaca-prod-3408831585-kncwt ... 10.112.2.84 ...

alpaca-prod-3408831585-l9fsq ... 10.112.2.85 ...

À ce stade, nous avons les bases de la découverte des services ! Nous pouvons
toujours utiliser des étiquettes pour identifier l’ensemble de pods qui nous
intéresse, récupérer tous les pods correspondant à ces étiquettes, et extraire
l’adresse IP. Conserver un ensemble correct d’étiquettes et en assurer la
synchronisation pourrait cependant s’avérer difficile et c’est la raison pour laquelle
l’objet Service a été créé.

7.5.3 kube-proxy et les adresses IP du cluster

Les adresses IP du cluster sont des adresses IP virtuelles stables qui équilibrent la
charge du trafic sur tous les points de terminaison d’un service. Ce miracle est
accompli par un composant fonctionnant sur chaque nœud du cluster et qui se
nomme kube-proxy (figure 7.1).

Figure 7.1 – Configuration et utilisation d’une adresse IP de cluster.

Dans la figure 7.1, kube-proxy surveille les nouveaux services dans le cluster via le
serveur d’API. Il programme ensuite une série de règles iptables dans le noyau de
cet hôte pour réécrire la destination des paquets de telle sorte qu’ils soient dirigés
vers l’un des points de terminaison de ce service. Si l’ensemble des points de
terminaison d’un service change (en raison des mouvements des pods ou en
raison d’un échec de la vérification de la disponibilité), l’ensemble des règles
iptables est réécrit.
L’adresse IP du cluster lui-même est généralement assignée par le serveur d’API
quand le service est créé. Toutefois, lors de la création du service, l’utilisateur peut
choisir une adresse IP de cluster spécifique. Une fois qu’elle est définie, l’IP du
cluster ne peut pas être modifiée sans supprimer et recréer l’objet Service.
La plage d’adresses du service Kubernetes est configurée à l’aide du
paramètre --service-cluster-ip-range du binaire kube-apiserver. La plage
d’adresses de service ne doit pas se chevaucher avec les sous-réseaux IP et
les plages assignées à chaque pont Docker ou à chaque nœud Kubernetes.
En outre, toute adresse IP de cluster explicite qui est demandée doit provenir
de cette plage et ne pas être déjà utilisée.

7.5.4 Variables d’environnement IP du cluster

Alors que la plupart des utilisateurs doivent employer les services DNS pour
trouver les adresses IP du cluster, il existe des mécanismes plus anciens dont on
peut encore se servir. On peut notamment citer l’injection de variables
d’environnement dans les pods quand ils démarrent.
Pour voir cela en action, examinons la console de l’instance bandicoot de kuard.
Entrez les commandes suivantes dans votre terminal :
$ BANDICOOT_POD=$(kubectl get pods -l app=bandicoot \
-o jsonpath='{.items[0].metadata.name}')
$ kubectl port-forward $BANDICOOT_POD 48858:8080

Pointez ensuite votre navigateur à http://localhost:48858 pour afficher la page


d’état de ce serveur. Développez la section « Server Env » et notez l’ensemble des
variables d’environnement du service alpaca. La page d’état doit afficher une liste
similaire au tableau 7.1.

Tableau 7.1 – Variables d’environnement d’un


service

Clé Valeur

ALPACA_PROD_PORT tcp://10.115.245.13:808

ALPACA_PROD_PORT_8080_TCP tcp://10.115.245.13:808

ALPACA_PROD_PORT_8080_TCP_ADDR 10.115.245.13

ALPACA_PROD_PORT_8080_TCP_PORT 8080

ALPACA_PROD_PORT_8080_TCP_PROTO tcp
ALPACA_PROD_SERVICE_HOST 10.115.245.13

ALPACA_PROD_SERVICE_PORT 8080

Les deux principales variables d’environnement à utiliser sont


ALPACA_PROD_SERVICE_HOST et ALPACA_PROD_SERVICE_PORT. Les
autres variables d’environnement sont créées pour être compatibles avec les
variables de lien Docker (fonctionnalité qui est à présent obsolète).
L’utilisation des variables d’environnement est cependant délicate car il faut que les
ressources soient créées dans un ordre spécifique. Les services doivent être créés
avant les pods qui les référencent. Cela peut engendrer de la complexité lors du
déploiement d’un ensemble de services qui constituent une application plus
importante. En outre, se contenter de n’utiliser que des variables d’environnement
déroute de nombreux utilisateurs. C’est pour cette raison que l’emploi du DNS est
probablement une meilleure solution.

_ 7.6 NETTOYAGE
Exécutez les commandes suivantes pour vous débarrasser de tous les objets créés
dans ce chapitre :
$ kubectl delete services,deployments -l app

_ 7.7 RÉSUMÉ
Kubernetes est un système dynamique qui remet en cause les méthodes
traditionnelles de nommage et de connexion des services sur le réseau. L’objet
Service fournit un moyen souple et puissant d’exposer des services à la fois dans
le cluster et hors du cluster. Avec les techniques étudiées dans ce chapitre, vous
pouvez connecter les services les uns aux autres et les exposer en dehors du
cluster.
Même si l’emploi des mécanismes de découverte dynamique des services dans
Kubernetes introduit de nouveaux concepts et peut, au premier abord, sembler
complexe, la compréhension et l’adaptation de ces techniques est la clé pour
libérer toute la puissance de Kubernetes. Une fois que votre application peut
trouver dynamiquement des services et réagir au placement dynamique de ces
applications, vous pouvez cesser de vous inquiéter de l’endroit où les choses
tournent et du moment où elles sont déplacées. Il s’agit d’une pièce critique du
puzzle pour commencer à penser aux services d’une façon logique et laisser
Kubernetes s’occuper des détails du placement des conteneurs.
8

ReplicaSets

Dans les chapitres précédents, nous avons abordé la façon


d’exécuter des conteneurs individuels sous la forme de pods. Mais
ces pods sont essentiellement des exemplaires uniques à usage
ponctuel car vous souhaitez le plus souvent que plusieurs réplicas
d’un conteneur s’exécutent à un moment donné. Il existe plusieurs
raisons de mettre en œuvre ce type de réplication :
Redondance
L’exécution de plusieurs instances implique que l’on peut tolérer des
pannes.
Mise à l’échelle
L’exécution de plusieurs instances a pour conséquence que l’on peut
gérer plus de requêtes.
Partitionnement
Différents réplicas peuvent gérer différentes parties d’un calcul en
parallèle.
Bien évidemment, vous pouvez créer manuellement plusieurs copies
d’un pod en utilisant plusieurs manifestes de pods différents (bien
que largement similaires), mais procéder de la sorte est à la fois
fastidieux et sujet aux erreurs. Normalement, un utilisateur qui
administre un ensemble de pods répliqués les considère comme une
entité unique qu’il doit définir et gérer. C’est précisément la nature
d’un ReplicaSet. Un ReplicaSet agit en tant que gestionnaire de
pods à l’échelle du cluster, en veillant à ce que les bons types et le
nombre correct de pods soient en cours d’exécution en permanence.
Comme les ReplicaSets facilitent la création et la gestion des
ensembles de pods répliqués, ils constituent les briques de base
employés pour décrire les modèles de déploiement des applications
courantes et fournir les fondations de l’auto-guérison pour nos
applications au niveau de l’infrastructure. Les pods gérés grâce à
des ReplicaSets sont automatiquement replanifiés quand certaines
pannes se produisent, comme dans les nœuds et les partitions
réseau.
La façon la plus simple de se représenter un ReplicaSet est
d’imaginer qu’il combine un moule à gâteau et un nombre désiré de
gâteaux dans un seul objet API. Lorsque nous définissons un
ReplicaSet, nous définissons une spécification pour les pods que
nous voulons créer (le « moule à gâteau »), et un nombre désiré de
réplicas. En outre, nous devons définir un moyen de trouver les pods
que le ReplicaSet doit contrôler. L’acte réel de gestion des pods
répliqués est un exemple de boucle de rapprochement
(reconciliation loop). Ces boucles sont fondamentales pour la plupart
des conceptions et des implémentations de Kubernetes.

_ 8.1 BOUCLES DE RAPPROCHEMENT


Les concepts centraux derrière les boucles de rapprochement sont
les notions d’état désiré, d’état observé ou d’état actuel. L’état désiré
est l’état que vous souhaitez. Avec un ReplicaSet, il s’agit du
nombre désiré de réplicas et de la définition des pods à répliquer.
Par exemple, l’état désiré est qu’il existe trois réplicas d’un pod
exécutant le serveur kuard.
En revanche, l’état actuel est l’état du système observé au moment
où on l’observe. Par exemple, il n’y a que deux pods kuard en cours
d’exécution.
La boucle de rapprochement s’exécute en permanence et observe
l’état actuel du monde afin d’agir pour essayer de faire correspondre
l’état observé à l’état désiré. Si l’on reprend l’exemple précédent, la
boucle de rapprochement crée un nouveau pod kuard afin de faire
correspondre l’état observé à l’état désiré (c’est-à-dire trois réplicas).
Il y a beaucoup d’avantages dans cette approche de boucle de
rapprochement quand on veut gérer l’état d’un conteneur. Il s’agit
d’un système intrinsèquement basé sur l’objectif à atteindre et l’auto-
guérison, tout en restant souvent facile à définir grâce à quelques
lignes de code.
À titre d’exemple, vous noterez que la boucle de rapprochement des
ReplicaSets est une boucle unique, qui gère pourtant les deux
actions de l’utilisateur pour faire évoluer à la hausse ou à la baisse le
nombre de ReplicaSets, ainsi que les pannes des nœuds ou les
nœuds rejoignant le cluster après avoir été absents.
Dans le reste du livre, nous verrons de nombreux exemples
pratiques de boucles de rapprochement.

_ 8.2 RAPPORT ENTRE LES PODS


ET LES REPLICASETS
Le découplage est l’un des thèmes clés de Kubernetes. En
particulier, il est important que tous les concepts de base de
Kubernetes soient modulaires les uns par rapport aux autres et qu’ils
soient permutables et remplaçables par d’autres composants. Dans
cet esprit, la relation entre les ReplicaSets et les pods est
relativement lâche. Bien que les ReplicaSets créent et gèrent des
pods, ils ne possèdent pas les pods qu’ils créent. Les ReplicaSets
utilisent des requêtes d’étiquettes pour identifier l’ensemble de pods
qu’ils doivent gérer. Ils utilisent ensuite la même API de pod que
nous avons employée directement dans le chapitre 5 pour créer les
pods qu’ils gèrent. Cette notion de « livraison à domicile » est une
autre idée de conception centrale dans Kubernetes. Dans un
découplage similaire, les ReplicaSets qui créent plusieurs pods et
les services qui équilibrent leur charge sont aussi totalement
séparés, découplés des objets API. Outre la prise en charge de la
modularité, le découplage des pods et des ReplicaSets permet
plusieurs fonctionnalités importantes qui sont décrites dans les
sections suivantes.

8.2.1 Adoption de conteneurs existants

Malgré l’intérêt de la configuration déclarative des logiciels, il y a des


fois où il est plus facile de créer quelque chose de manière
impérative. En particulier, les premières fois, il peut s’avérer plus
simple de déployer un seul pod avec une image de conteneur sans
gérer de ReplicaSet. Mais à certains moments, il est préférable de
transformer votre unique conteneur en un service répliqué et créer
pour cela une matrice de conteneurs similaires. Vous pouvez même
définir un équilibreur de charge qui régule le trafic sur ce pod unique.
Si les ReplicaSets sont propriétaires des pods qu’ils ont créés, alors
la seule façon de commencer à répliquer votre pod consisterait à le
supprimer, puis à le relancer via un ReplicaSet. Cela pourrait créer
des perturbations, car à un moment donné il n’y aurait plus de copie
de votre conteneur en cours d’exécution. Cependant, comme les
ReplicaSets sont découplés des pods qu’ils gèrent, vous pouvez
simplement créer un ReplicaSet qui « adoptera » le pod existant et
dimensionnera le nombre de copies supplémentaires de ces
conteneurs. De cette façon, vous pouvez passer en douceur d’un
pod créé de manière impérative à un ensemble répliqué de pods
gérés par un ReplicaSet.

8.2.2 Mise en quarantaine de conteneurs

Souvent, quand un serveur a des problèmes, les contrôles d’intégrité


au niveau du pod redémarrent automatiquement ce pod. Mais si vos
contrôles d’intégrité sont incomplets, un pod peut avoir des
défaillances, mais toujours faire partie de l’ensemble répliqué. Dans
ces situations, même si cela fonctionne de supprimer simplement le
pod, cela n’est pas souhaitable car vos développeurs ne
disposeraient que des logs pour déboguer le problème. Au lieu de
cela, vous pouvez modifier l’ensemble des étiquettes du pod
défaillant, ce qui va le dissocier du ReplicaSet (et du service) si bien
que vous pourrez déboguer le pod. Le contrôleur du ReplicaSet
remarquera qu’il manque un pod et en créera une nouvelle copie ;
mais comme le pod sera toujours en cours d’exécution, il restera
disponible pour que les développeurs assurent un débogage
interactif, ce qui est bien plus efficace qu’un débogage à partir des
logs.

_ 8.3 CONCEPTION AVEC


DES REPLICASETS
Les ReplicaSets sont conçus pour représenter un micro-service
unique et évolutif à l’intérieur de votre architecture. La
caractéristique clé des ReplicaSets est que chaque pod créé par le
contrôleur de ReplicaSets est entièrement homogène. De façon
classique, ces pods sont alors exploités par un équilibreur de charge
de services Kubernetes, qui répartit le trafic entre les différents pods
qui constituent le service. D’une manière générale, les ReplicaSets
sont conçus pour des services stateless (ou presque stateless). Les
éléments créés par le ReplicaSet sont interchangeables ; lorsqu’un
ReplicaSet est revu à la baisse, un pod arbitraire est sélectionné
pour qu’il soit supprimé. Le comportement de votre application ne
doit pas changer à cause d’une telle opération.

_ 8.4 SPÉCIFICATIONS
DES REPLICASETS
Comme tous les concepts de Kubernetes, les ReplicaSets sont
définis à l’aide d’une spécification. Tous les ReplicaSets doivent
avoir un nom unique (défini grâce au champ metadata.name), une
section spec qui décrit le nombre de pods (réplicas) qui doivent être
exécutés à l’échelle du cluster à un moment donné, et un modèle de
pod qui décrit le pod à créer lorsque le nombre défini de réplicas
n’est pas atteint. L’exemple 8-1 illustre une définition minimale de
ReplicaSet.
Exemple 8-1. kuard-rs.yaml
apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
name: kuard
spec:
replicas: 1
template:
metadata:
labels:
app: kuard
version: "2"
spec:
containers:
- name: kuard
image: "gcr.io/kuar-demo/kuard-amd64:2"

8.4.1 Modèles de pods

Comme nous l’avons mentionné précédemment, lorsque le nombre


de pods de l’état actuel est inférieur au nombre de pods de l’état
désiré, le contrôleur de ReplicaSets crée de nouveaux pods. Les
pods sont créés à l’aide d’un modèle de pod qui se trouve dans la
spécification du ReplicaSet. Les pods sont créés exactement de la
même manière que lorsque vous avez créé un pod à partir d’un
fichier YAML dans les chapitres précédents. Cependant, au lieu
d’utiliser un fichier, le contrôleur de ReplicaSets Kubernetes crée et
soumet directement au serveur API un manifeste de pod basé sur le
modèle de pod. L’exemple suivant illustre un modèle de pod dans un
ReplicaSet :
template:
metadata:
labels:
app: helloworld
version: v1
spec:
containers:
- name: helloworld
image: kelseyhightower/helloworld:v1
ports:
- containerPort: 80

8.4.2 Étiquettes

Tout cluster de taille raisonnable compte de nombreux pods


différents qui s’exécutent à un moment donné si bien que l’on peut
se demander comment la boucle de rapprochement des ReplicaSets
découvre l’ensemble des pods d’un ReplicaSet particulier. Les
ReplicaSets surveillent l’état du cluster à l’aide d’un ensemble
d’étiquettes de pods. Les étiquettes sont utilisées pour filtrer les
listes de pods et contrôler le fonctionnement des pods dans un
cluster. Quand les ReplicaSets sont créés au départ, le ReplicaSet
récupère une liste de pods à partir de l’API Kubernetes et filtre les
résultats grâce à des étiquettes. En fonction du nombre de pods
retournés par la requête, le ReplicaSet supprime ou crée des pods
pour satisfaire le nombre de réplicas souhaité. Les étiquettes
utilisées pour le filtrage sont définies dans la section spec du
ReplicaSet et sont importantes pour comprendre le fonctionnement
des ReplicaSets.

Le sélecteur dans la section spec du ReplicaSet doit être


un sous-ensemble approprié des étiquettes du modèle de pod.

_ 8.5 CRÉATION D’UN REPLICASET


Les ReplicaSets sont créés en soumettant un objet ReplicaSet à
l’API Kubernetes. Dans cette section, nous allons créer un
ReplicaSet à l’aide d’un fichier de configuration et de la commande
kubectl apply.
Le fichier de configuration du ReplicaSet de l’exemple 8-1 va
garantir qu’une copie du conteneur gcr.io/kuar-demo/kuard-amd64:2
est en cours d’exécution à un moment donné.
Utilisez la commande kubectl apply pour soumettre le ReplicaSet
kuard à l’API Kubernetes :
$ kubectl apply -f kuard-rs.yaml
replicaset "kuard" created

Une fois que le ReplicaSet kuard a été accepté, le contrôleur de


ReplicaSets détecte qu’il n’y a pas de pods kuard en cours
d’exécution qui correspondent à l’état désiré, et un nouveau pod
kuard sera créé en fonction du contenu du modèle de pod :
$ kubectl get pods

NAME READY STATUS RESTARTS AGE

kuard-
1/1 Running 0 11s
yvzgd

_ 8.6 INSPECTION D’UN REPLICASET


Comme pour les pods et les autres objets de l’API Kubernetes, si
vous êtes intéressé par des détails supplémentaires sur un
ReplicaSet, la commande describe fournit beaucoup plus
d’informations sur son état. Voici un exemple d’utilisation de la
commande describe pour obtenir des informations détaillées sur le
ReplicaSet que nous avons créé précédemment :
$ kubectl describe rs kuard

Name: kuard
Namespace: default

Image(s): kuard:1.9.15

Selector: app=kuard,version=2

Replicas: 1 current / 1 desired

Labels: app=kuard,version=2

Pods Status: 1 Running / 0 Waiting / 0 Succeeded / 0 Failed

No volumes.

Vous pouvez voir le sélecteur d’étiquettes du ReplicaSet, ainsi que


l’état de tous les réplicas gérés par le ReplicaSet.

8.6.1 Recherche d’un ReplicaSet à partir d’un pod

Parfois, vous pouvez vous demander si un pod est géré par un


ReplicaSet, et si tel est le cas, de quel ReplicaSet il s’agit.
Pour permettre ce type de découverte, le contrôleur de ReplicaSets
ajoute une annotation à chaque pod qu’il crée. La clé de cette
annotation est kubernetes.io/created-by. Si vous exécutez ce qui
suit, vous allez rechercher l’entrée kubernetes.io/created-by dans la
section des annotations :
$ kubectl get pods <nom-du-pod> -o yaml

S’il y a lieu, cela va lister le nom du ReplicaSet qui gère ce pod.


Vous noterez cependant que ces annotations ne sont pas infaillibles
car elles sont générées lors de la création du pod par le ReplicaSet
si bien qu’elles peuvent être supprimées par un utilisateur
Kubernetes à tout moment.

8.6.2 Recherche d’un ensemble de pods à partir d’un


ReplicaSet
Vous pouvez aussi déterminer quel ensemble de pods est géré par
un ReplicaSet. Tout d’abord, vous pouvez obtenir l’ensemble des
étiquettes à l’aide de la commande kubectl describe. Dans l’exemple
précédent, le sélecteur d’étiquette était app=kuard,version=2. Pour
trouver les pods correspondant à ce sélecteur, utilisez le paramètre -
-selector ou le raccourci -l :
$ kubectl get pods -l app=kuard,version=2

C’est exactement la même requête que le ReplicaSet exécute pour


déterminer le nombre actuel de pods.

_ 8.7 MISE À L’ÉCHELLE


DES REPLICASETS
Les ReplicaSets sont revus à la hausse ou à la baisse en mettant à
jour la clé spec.replicas de l’objet ReplicaSet stocké dans
Kubernetes. Lorsqu’un ReplicaSet est revu à la hausse, de
nouveaux pods sont soumis à l’API Kubernetes à l’aide du modèle
de pod défini sur le ReplicaSet.

8.7.1 Mise à l’échelle impérative avec kubectl scale

La meilleure façon d’y parvenir est d’utiliser la commande kubectl


scale. Par exemple, pour une mise à l’échelle pouvant comporter
jusqu’à quatre réplicas, vous pouvez exécuter :
$ kubectl scale replicasets kuard --replicas=4

Même si ces commandes impératives sont utiles pour les


démonstrations et les réactions rapides aux situations d’urgence (par
exemple, pour répondre à une augmentation soudaine de la charge),
il est important aussi de mettre à jour tous les fichiers de
configuration pour qu’ils correspondent au nombre de réplicas que
vous définissez de manière impérative via la commande scale. La
lecture du scénario suivant devrait vous persuader du bien-fondé de
la chose :

Alice est de garde quand, tout à coup, il y a une grande


augmentation de la charge sur le service qu’elle gère. Alice
utilise la commande scale pour augmenter le nombre de
serveurs afin d’en avoir une dizaine et ainsi répondre aux
requêtes, si bien que la situation est résolue. Cependant,
Alice oublie de mettre à jour les configurations du ReplicaSet
qui sont vérifiées dans le contrôle de code source. Plusieurs
jours plus tard, Bob prépare les déploiements
hebdomadaires. Bob édite les configurations du ReplicaSet
stockées dans le contrôle de version pour utiliser l’image du
nouveau conteneur, mais il ne remarque pas que le nombre
de réplicas dans le fichier s’élève alors à 5, et non à 10,
comme Alice l’a paramétré en réponse à la charge. Bob
procède au déploiement, qui met à jour l’image du conteneur
et réduit le nombre de réplicas de moitié, ce qui provoque une
surcharge immédiate ou une panne.

Espérons que cela illustre la nécessité de s’assurer que toutes les


modifications impératives sont immédiatement suivies d’une
modification déclarative du contrôle de code source. En effet, si le
besoin n’est pas critique, nous recommandons généralement de ne
faire que des modifications déclaratives, comme cela est décrit dans
la section suivante.

8.7.2 Mise à l’échelle de façon déclarative avec kubectl


apply

Dans un monde déclaratif, on apporte des modifications en éditant le


fichier de configuration dans le contrôle de version, puis on applique
ces modifications au cluster. Pour mettre à l’échelle le ReplicaSet
kuard, modifiez le fichier de configuration kuard-rs.yaml et définissez
le compteur replicas à 3 :
...
spec:
replicas: 3
...

Dans un environnement multi-utilisateur, il serait souhaitable d’avoir


une trace écrite de cette modification et éventuellement de vérifier
les modifications dans le contrôle de version. De toute façon, vous
pouvez utiliser la commande kubectl apply pour soumettre le
ReplicaSet kuard mis à jour au serveur API :
$ kubectl apply -f kuard-rs.yaml
replicaset "kuard" configured

Maintenant que le ReplicaSet kuard mis à jour est en place, le


contrôleur de ReplicaSets détecte que le nombre de pods souhaités
a changé et qu’il doit prendre des mesures pour parvenir à cet état
désiré. Si vous avez utilisé la commande impérative scale dans la
section précédente, le contrôleur de ReplicaSets va supprimer un
pod pour n’en avoir plus que trois. Dans le cas contraire, il soumettra
deux nouveaux pods à l’API Kubernetes en utilisant le modèle de
pod défini sur le ReplicaSet kuard. Quoi qu’il en soit, utilisez la
commande kubectl get pods pour répertorier les pods kuard en
cours d’exécution. Vous devriez voir une sortie similaire à ce qui
suit :
$ kubectl get pods

NAME READY STATUS RESTARTS AGE

kuard-3a2sb 1/1 Running 0 26s

kuard-wuq9v 1/1 Running 0 26s

kuard-yvzgd 1/1 Running 0 2m

8.7.3 Mise à l’échelle automatique d’un ReplicaSet


Même s’il y a des moments où il est souhaitable d’avoir un contrôle
explicite sur le nombre de réplicas dans un ReplicaSet, la plupart du
temps vous voulez simplement avoir « assez » de réplicas. La
définition varie en fonction des besoins des conteneurs dans le
ReplicaSet. Par exemple, avec un serveur Web comme nginx, la
mise à l’échelle est faite en fonction de l’utilisation du processeur.
Pour un cache en mémoire, l’évolutivité est liée à la consommation
de la mémoire. Dans certains cas, vous souhaiterez effectuer un
redimensionnement en réponse à des métriques d’application
personnalisées. Kubernetes sait gérer tous ces scénarios via un
mécanisme nommé HPA (horizontal pod autoscaling, mise à
l’échelle automatique et horizontale des pods).

HPA exige la présence du pod heapster sur votre cluster.


heapster garde la trace des métriques et fournit une API pour
des métriques de consommation que HPA utilise pour prendre
des décisions de mise à l’échelle. La plupart des installations de
Kubernetes comprennent heapster par défaut. Vous pouvez
vérifier sa présence en listant les pods dans l’espace de noms
kube-system :
$ kubectl get pods --namespace=kube-system
Vous devriez voir un pod nommé heapster quelque part dans
cette liste. Si vous ne le voyez pas, la mise à l’échelle
automatique ne fonctionnera pas correctement.

La « mise à l’échelle automatique et horizontale des pods » est une


dénomination très longue et vous vous demandez sans doute
pourquoi on ne parle pas plus simplement de « mise à l’échelle
automatique ». Kubernetes fait une distinction entre la mise à
l’échelle horizontale, qui implique la création de réplicas
supplémentaires d’un pod, et la mise à l’échelle verticale, qui
implique d’augmenter les ressources nécessaires d’un pod
particulier (par exemple, augmenter le CPU pour le pod). La mise à
l’échelle verticale n’est pas implémentée actuellement dans
Kubernetes, mais elle est ordonnancée. En outre, de nombreuses
solutions permettent aussi une mise à l’échelle automatique du
cluster, et le nombre de machines dans le cluster évolue alors en
fonction des besoins en ressources, mais cette solution n’est pas
abordée dans ce livre.

◆ Mise à l’échelle automatique basée sur le CPU

La mise à l’échelle basée sur l’utilisation du processeur est le cas le


plus courant pour l’évolutivité du pod. En général, c’est très utile
pour les systèmes basés sur les requêtes qui consomment le CPU
proportionnellement au nombre de requêtes qu’ils reçoivent, tout en
utilisant une quantité relativement statique de mémoire.
Pour faire évoluer un ReplicaSet, vous pouvez exécuter la
commande suivante :
$ kubectl autoscale rs kuard --min=2 --max=5 --cpu-percent=80

Cette commande crée un système automatique de mise à l’échelle


qui évolue de deux à cinq réplicas avec un seuil de CPU à 80 %.
Pour afficher, modifier ou supprimer cette ressource, vous pouvez
utiliser les commandes habituelles kubectl et la ressource
horizontalpodautoscalers. horizontalpodautoscalers est un peu
difficile à taper, mais vous pouvez l’abréger en hpa :
$ kubectl get hpa
En raison de la nature découplée de Kubernetes, il n’y a
pas de lien direct entre HPA et les ReplicaSets. Même si c’est
parfait pour la modularité, cela peut engendrer des modèles qui
ne sont pas à suivre. En particulier, il n’est pas conseillé de
combiner à la fois la mise à l’échelle automatique et la gestion
impérative ou déclarative du nombre de réplicas. Si vous tentez
de modifier le nombre de réplicas pendant que HPA fait de
même, il est très probable que vous obtiendrez des résultats
inattendus.

_ 8.8 SUPPRESSION DES REPLICASETS


Lorsqu’un ensemble de ReplicaSets n’est plus nécessaire, il peut
être supprimé avec la commande kubectl delete. Par défaut, cela
supprime aussi les pods gérés par le ReplicaSet :
$ kubectl delete rs kuard
replicaset "kuard" deleted

L’exécution de la commande kubectl get pods montre que tous les


pods kuard créés par le ReplicaSet kuard ont aussi été supprimés :
$ kubectl get pods

Si vous ne voulez pas supprimer les pods qui sont gérés par le
ReplicaSet, vous pouvez définir le paramètre --cascade à false pour
garantir que seul l’objet ReplicaSet est supprimé et non les pods :
$ kubectl delete rs kuard --cascade=false

_ 8.9 RÉSUMÉ
La création de pods avec des ReplicaSets fournit la base de la
conception d’applications robustes avec un basculement
automatique et facilite le déploiement de ces applications en
autorisant des modèles de déploiement évolutifs. Les ReplicaSets
devraient être utilisés pour n’importe quel pod important, même s’il
s’agit d’un pod unique. Certaines personnes utilisent même par
défaut des ReplicaSets à la place des pods. En général, un cluster
comportera de nombreux ReplicaSets et vous n’avez donc aucune
raison de vous en priver.
9

DaemonSets

Les ReplicaSets servent en général à la création d’un service (par


exemple, un serveur Web) ayant plusieurs réplicas pour assurer la
redondance. Mais ce n’est pas la seule raison pour laquelle il est
souhaitable de répliquer un ensemble de pods dans un cluster : on
peut notamment répliquer des pods pour ordonnancer un pod unique
sur chaque nœud du cluster. Habituellement, on procède de cette
manière pour héberger un agent ou un démon sur chaque nœud, et
l’objet Kubernetes pour réaliser cela s’appelle un DaemonSet.
Un DaemonSet garantit qu’une copie d’un pod s’exécute sur un
ensemble de nœuds dans un cluster Kubernetes. Les DaemonSets
sont utilisés pour déployer des démons système, comme des
collecteurs de journaux et des agents de surveillance, qui doivent
généralement s’exécuter sur chaque nœud. Les DaemonSets
partagent des fonctionnalités avec les ReplicaSets car ils créent des
pods qui sont censés être des services persistants s’assurant que
l’état désiré correspond à l’état observé du cluster.
Étant donné les similitudes entre les DaemonSets et les
ReplicaSets, il est important de comprendre quand utiliser les uns
par rapport aux autres. Les ReplicaSets doivent être utilisés lorsque
votre application est complètement découplée du nœud et que vous
pouvez exécuter plusieurs copies sur un nœud donné sans attention
particulière. Les DaemonSets doivent être utilisés lorsqu’une seule
copie de votre application doit s’exécuter sur l’ensemble ou un sous-
ensemble des nœuds du cluster.
Vous ne devez généralement pas utiliser de restrictions
d’ordonnancement ou d’autres paramètres pour vous assurer que
les pods ne résident pas sur le même nœud. Si vous voulez garantir
qu’il n’y a qu’un seul pod par nœud, il faut utiliser un DaemonSet. De
la même manière, si vous voulez créer un service homogène
répliqué pour acheminer le trafic des utilisateurs, il est préférable
d’employer un ReplicaSet.

_ 9.1 ORDONNANCEUR DAEMONSET


Par défaut, un DaemonSet crée une copie d’un pod sur chaque
nœud à moins qu’un sélecteur de nœud ne soit utilisé, ce qui limite
les nœuds éligibles à ceux qui ont un jeu d’étiquettes correspondant.
C’est au moment de la création du pod, en spécifiant le champ
nodeName dans les spécifications du pod, que les DaemonSets
déterminent le nœud sur lequel un pod s’exécutera. En
conséquence, les pods créés par les DaemonSets sont ignorés par
l’ordonnanceur Kubernetes.
Comme les ReplicaSets, les DaemonSets sont gérés par une boucle
de contrôle de rapprochement qui compare l’état désiré (un pod est
présent sur tous les nœuds) à l’état observé (le pod est-il présent sur
un nœud particulier ?). Compte tenu de cette information, le
contrôleur de DaemonSets crée un pod sur chaque nœud qui n’a
pas de pod correspondant.
Si un nouveau nœud est ajouté au cluster, le contrôleur de
DaemonSets remarque qu’il manque un pod et ajoute le pod au
nouveau nœud.
Les DaemonSets et les ReplicaSets sont une belle
démonstration de la valeur de l’architecture découplée de
Kubernetes. On pourrait penser qu’une bonne conception serait
qu’un ReplicaSet possède les pods qu’il gère, et que les pods
soient des sous-ressources d’un ReplicaSet. De la même
manière, les pods gérés par un DaemonSet seraient des sous-
ressources de ce DaemonSet. Toutefois, ce type
d’encapsulation exigerait que les outils de gestion des pods
soient écrits à deux moments différents, l’un pour les
DaemonSets et l’autre pour les ReplicaSets. Au lieu de cela,
Kubernetes utilise une approche découplée où les pods sont
des objets de niveau supérieur. Cela signifie que tous les outils
employés pour l’analyse des pods dans le contexte des
ReplicaSets (par exemple, kubectl logs <nom-du-pod>)
s’appliquent aussi aux pods créés par les DaemonSets.

_ 9.2 CRÉATION DES DAEMONSETS


Les DaemonSets sont créés en soumettant une configuration de
DaemonSet au serveur API Kubernetes. Le DaemonSet suivant va
créer un agent de journalisation fluentd sur chaque nœud du cluster
cible (exemple 9-1).
Exemple 9-1. fluentd.yaml
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: fluentd
namespace: kube-system
labels:
app: fluentd
spec:
template:
metadata:
labels:
app: fluentd
spec:
containers:
- name: fluentd
image: fluent/fluentd:v0.14.10
resources:
limits:
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers

Les DaemonSets ont besoin d’un nom unique sur l’ensemble des
DaemonSets d’un espace de noms Kubernetes donné. Chaque
DaemonSet doit inclure un modèle de spécification de pod qui sera
utilisé pour créer des pods en fonction des besoins. C’est à cet
endroit que les similitudes entre les ReplicaSets et les DaemonSets
prennent fin. Contrairement aux ReplicaSets, les DaemonSets
créent par défaut des pods sur chaque nœud du cluster, à moins
qu’un sélecteur de nœud ne soit utilisé.
Une fois que vous avez mis en place une configuration de
DaemonSet valide, vous pouvez utiliser la commande kubectl apply
pour soumettre le DaemonSet à l’API Kubernetes. Dans cette
section, nous allons créer un DaemonSet pour nous assurer que le
serveur HTTP fluentd s’exécute sur tous les nœuds de notre cluster :
$ kubectl apply -f fluentd.yaml
daemonset "fluentd" created

Une fois que le DaemonSet fluentd a été soumis avec succès à l’API
Kubernetes, vous pouvez interroger son état actuel à l’aide de la
commande kubectl describe :
$ kubectl describe daemonset fluentd –namespace=kube-system

Name: fluentd

Image(s): fluent/fluentd:v0.14.10

Selector: app=fluentd

Node-Selector: <none>

Labels: app=fluentd

Desired Number of Nodes Scheduled: 3


Current Number of Nodes Scheduled: 3
Number of Nodes Misscheduled: 0
Pods Status: 3 Running / 0 Waiting / 0 Succeeded / 0 Failed

Cette sortie indique qu’un pod fluentd a été déployé avec succès sur
les trois nœuds de notre cluster. Nous pouvons vérifier ceci en
utilisant la commande kubectl get pods avec le paramètre -o pour
afficher les nœuds où chaque pod fluentd a été assigné :
$ kubectl get pods -o wide

NAME AGE NODE

k0-default-pool-35609c18-
fluentd-1q6c6 13m
z7tb
fluentd-mwi7h 13m k0-default-pool-35609c18-
ydae

k0-default-pool-35609c18-
fluentd-zr6l7 13m
pol3

Le DaemonSet fluentd en place, l’ajout d’un nouveau nœud au


cluster se traduira par le déploiement automatique d’un pod fluentd
sur ce nœud :
$ kubectl get pods -o wide

NAME AGE NODE

k0-default-pool-35609c18-
fluentd-1q6c6 13m
z7tb

k0-default-pool-35609c18-
fluentd-mwi7h 13m
ydae

k0-default-pool-35609c18-
fluentd-oipmq 43s
0xnl

k0-default-pool-35609c18-
fluentd-zr6l7 13m
pol3

C’est exactement le comportement que l’on souhaite quand on gère


des démons de journalisation et d’autres services à l’échelle du
cluster. Aucune action n’a été requise de notre part ; c’est de cette
façon que le contrôleur de DaemonSets Kubernetes réconcilie son
état observé avec notre état désiré.

_ 9.3 LIMITATION DES DAEMONSETS


À DES NŒUDS PARTICULIERS
Le cas d’utilisation le plus courant d’un DaemonSet est l’exécution
d’un pod sur chaque nœud d’un cluster Kubernetes. Toutefois, il
existe certains cas où il est souhaitable de déployer un pod
uniquement sur un sous-ensemble de nœuds. Par exemple, vous
pouvez avoir une charge de travail nécessitant un GPU ou un accès
à un stockage rapide qui ne soit disponible que sur un sous-
ensemble de nœuds de votre cluster. Dans ces cas-là, les étiquettes
de nœuds peuvent être employées pour étiqueter des nœuds
spécifiques qui satisfont aux exigences de la charge de travail.

9.3.1 Ajout d’étiquettes aux nœuds

La première étape pour limiter des DaemonSets à des nœuds


spécifiques consiste à ajouter le jeu d’étiquettes souhaité à un sous-
ensemble de nœuds. Ceci peut être réalisé avec la commande
kubectl label.
La commande suivante ajoute l’étiquette ssd=true à un seul nœud :
$ kubectl label nodes k0-default-pool-35609c18-z7tb ssd=true
node "k0-default-pool-35609c18-z7tb" labeled

Comme pour toutes les autres ressources Kubernetes, l’affichage de


la liste des nœuds sans sélecteur d’étiquettes renvoie tous les
nœuds du cluster :
$ kubectl get nodes

NAME STATUS AGE

k0-default-pool-35609c18-
Ready 23m
0xnl

k0-default-pool-35609c18-
Ready 1d
pol3

k0-default-pool-35609c18-
Ready 1d
ydae

k0-default-pool-35609c18-
Ready 1d
z7tb
En utilisant un sélecteur d’étiquettes, on peut filtrer les nœuds en
fonction des étiquettes. Pour répertorier uniquement les nœuds qui
ont l’étiquette ssd définie à true, utilisez la commande kubectl get
nodes avec le paramètre --selector :
$ kubectl get nodes --selector ssd=true

NAME STATUS AGE

k0-default-pool-35609c18-
Ready 1d
z7tb

9.3.2 Sélecteurs de nœuds

Les sélecteurs de nœuds peuvent être utilisés pour limiter les


nœuds sur lesquels un pod peut s’exécuter dans un cluster
Kubernetes donné. Les sélecteurs de nœuds sont définis dans la
spécification du pod lors de la création d’un DaemonSet. La
configuration DaemonSet suivante limite l’exécution de nginx aux
seuls nœuds dont l’étiquette vérifie la condition ssd=true (exemple 9-
2).
Exemple 9-2. nginx-fast-storage.yaml
apiVersion: extensions/v1beta1
kind: "DaemonSet"
metadata:
labels:
app: nginx
ssd: "true"
name: nginx-fast-storage
spec:
template:
metadata:
labels:
app: nginx
ssd: "true"
spec:
nodeSelector:
ssd: "true"
containers:
- name: nginx
image: nginx:1.10.0

Voyons ce qui se passe quand on soumet le DaemonSet nginx-fast-


storage à l’API Kubernetes :
$ kubectl apply -f nginx-fast-storage.yaml
daemonset "nginx-fast-storage" created

Puisqu’il n’y a qu’un seul nœud avec l’étiquette ssd=true, le pod


nginx-fast-storage ne s’exécutera que sur ce nœud :
$ kubectl get pods -o wide

NAME STATUS NODE

nginx-fast-storage-7b90t Running k0-default-pool-35609c18-z7tb

L’ajout de l’étiquette ssd=true à des nœuds supplémentaires


provoquera le déploiement du pod nginx-fast-storage sur ces
nœuds. L’inverse est aussi vrai : si une étiquette requise est
supprimée d’un nœud, le pod sera supprimé par le contrôleur de
DaemonSets.

La suppression des étiquettes d’un nœud qui sont


requises par le sélecteur de nœuds d’un DaemonSet
provoquera la suppression du pod de ce nœud par le
DaemonSet qui le gère.

_ 9.4 MISE À JOUR D’UN DAEMONSET


Les DaemonSets sont parfaits pour déployer des services sur
l’ensemble d’un cluster, mais qu’en est-il des mises à jour ? Avant la
version Kubernetes 1.6, la seule façon de mettre à jour les pods
gérés par un DaemonSet était de mettre à jour le DaemonSet, puis
de supprimer manuellement chaque pod qui était géré par le
DaemonSet afin qu’il soit recréé avec la nouvelle configuration. Avec
la sortie de la version Kubernetes 1.6, les DaemonSets ont gagné un
équivalent de l’objet Deployment qui gère le déploiement des
DaemonSets à l’intérieur du cluster.

9.4.1 Mise à jour d’un DaemonSet en supprimant


des pods individuels

Si vous exécutez une version antérieure à la version 1.6 de


Kubernetes, vous pouvez effectuer une suppression des pods gérés
par le DaemonSet avec une boucle for sur votre propre machine
pour mettre à jour un pod toutes les 60 secondes :
PODS=$(kubectl get pods -o jsonpath --template='{.items[*].metadata.name}')
for x in $PODS; do
kubectl delete pods ${x}
sleep 60
done

Une autre approche plus facile consiste simplement à supprimer


l’ensemble du DaemonSet et à en créer un nouveau avec la
configuration mise à jour. Cependant, cette approche présente un
inconvénient majeur : l’indisponibilité. Lorsqu’un DaemonSet est
supprimé, tous les pods gérés par ce DaemonSet sont également
supprimés. En fonction de la taille de vos images de conteneurs, le
fait de recréer un DaemonSet peut vous faire dépasser vos seuils
SLA, si bien qu’il peut être utile d’envisager d’extraire des images de
conteneurs mises à jour sur votre cluster avant de mettre à jour un
DaemonSet.

9.4.2 Déploiement des mises à jour d’un DaemonSet


Avec Kubernetes 1.6, les DaemonSets peuvent maintenant être
déployés à l’aide de la même stratégie de mise à jour que celle qui
est utilisée par les déploiements. Cependant, pour des raisons de
compatibilité avec les anciennes versions, la stratégie de mise à jour
par défaut actuelle est la méthode delete décrite dans la section
précédente. Afin de paramétrer un DaemonSet pour qu’il utilise la
stratégie de mise à jour, vous devez configurer cette stratégie à
l’aide du champ spec.updateStrategy.type. Ce champ doit avoir la
valeur RollingUpdate. Lorsqu’un DaemonSet a une stratégie de mise
à jour avec un champ RollingUpdate, tout changement du champ
spec.template (ou l’un de ses sous-champs) du DaemonSet lancera
une mise à jour.
Comme pour les mises à jour propagées avec des déploiements
(voir le chapitre 12), la stratégie de mise à niveau met à jour
progressivement les membres d’un DaemonSet jusqu’à ce que tous
les pods exécutent la nouvelle configuration. Il existe deux
paramètres qui contrôlent la mise à jour d’un DaemonSet :
spec.minReadySeconds, qui détermine la durée pendant laquelle
un pod doit être « prêt » avant que la mise à jour ne procède à la
mise à niveau des pods suivants ;
spec.updateStrategy.rollingUpdate.maxUnavailable, qui indique
combien de pods peuvent être mis à niveau simultanément par la
mise à jour.
Vous aurez probablement envie de définir spec.minReadySeconds à
une valeur suffisamment longue, par exemple de 30 à 60 secondes,
pour vous assurer que votre pod est vraiment opérationnel avant
que le déploiement n’ait lieu.
Le paramétrage de
spec.updateStrategy.rollingUpdate.maxUnavailable dépend
probablement plus de l’application. Si vous choisissez de définir ce
paramètre à 1, il s’agit d’une stratégie sûre et générique, mais il faut
aussi un certain temps pour achever le déploiement (nombre de
nœuds × minReadySeconds). L’augmentation de l’indisponibilité
maximale va accélérer votre déploiement, mais cela va augmenter
l’impact de l’échec d’un déploiement. Les caractéristiques de votre
application et de votre environnement de cluster vous indiqueront si
vous souhaitez privilégier la vitesse par rapport à la sécurité, ou
inversement. Une bonne approche pourrait consister à définir
maxUnavailable à 1 et à ne l’augmenter que si les utilisateurs ou les
administrateurs se plaignent de la vitesse de déploiement des
DaemonSets.
Une fois qu’une mise à jour a commencé, vous pouvez employer les
commandes kubectl rollout pour voir le statut actuel du déploiement
d’un DaemonSet.
Par exemple, kubectl rollout status daemonSets my-daemon-set
affiche l’état actuel du déploiement d’un DaemonSet nommé my-
daemon-set.

_ 9.5 SUPPRESSION D’UN DAEMONSET


La suppression d’un DaemonSet est assez simple à l’aide de la
commande kubectl delete. Assurez-vous juste de fournir le nom
correct du DaemonSet que vous souhaitez supprimer :
$ kubectl delete -f fluentd.yaml

La suppression d’un DaemonSet supprimera aussi tous


les pods gérés par ce DaemonSet. Réglez le paramètre --
cascade à false pour garantir que seuls les DaemonSets sont
supprimés et non les pods.

_ 9.6 RÉSUMÉ
Les DaemonSets fournissent une abstraction facile à utiliser pour
l’exécution d’un ensemble de pods sur chaque nœud d’un cluster
Kubernetes, ou si le cas l’exige, sur un sous-ensemble de nœuds
basés sur des étiquettes. Le DaemonSet fournit son propre
contrôleur et son propre ordonnanceur pour s’assurer que les
services clés, comme les agents de surveillance, sont toujours
opérationnels sur les bons nœuds de votre cluster.
Pour certaines applications, il est simplement souhaitable de
programmer un certain nombre de réplicas car on ne se soucie pas
vraiment de leur emplacement tant qu’ils ont suffisamment de
ressources pour fonctionner de manière fiable. Cependant, il existe
une autre classe d’applications, comme les agents et les
applications de surveillance, qui doivent être présents sur chaque
machine d’un cluster pour qu’il fonctionne correctement. Ces
DaemonSets ne sont pas vraiment des applications traditionnelles,
mais ils servent plutôt à ajouter des fonctionnalités au cluster
Kubernetes lui-même. Comme le DaemonSet est un objet déclaratif
actif géré par un contrôleur, il est facile de signifier votre intention
qu’un agent s’exécute sur chaque ordinateur sans avoir à le
déployer explicitement sur chaque machine. Ceci est
particulièrement utile dans le contexte d’un cluster Kubernetes mis à
l’échelle de manière automatique où les nœuds peuvent
constamment évoluer sans intervention de l’utilisateur. Dans de tels
cas, le DaemonSet ajoute automatiquement les agents appropriés à
chaque nœud au fur et à mesure qu’il est ajouté au cluster par le
processus de mise à l’échelle automatique.
10

Jobs

Jusqu’à présent, nous nous sommes concentrés sur des processus qui
durent longtemps, comme les bases de données et les applications Web.
Ces types de charges de travail s’exécutent jusqu’à ce qu’ils soient mis à
jour ou que le service ne soit plus nécessaire. Alors que ces processus longs
constituent la grande majorité des charges de travail qui s’exécutent sur un
cluster Kubernetes, on a aussi souvent besoin d’exécuter des tâches de
courte durée et ponctuelles. L’objet Job est conçu pour gérer ces types de
tâches.
Un job crée des pods qui s’exécutent jusqu’à ce qu’ils se terminent avec
succès (c’est-à-dire avec un code de sortie égal à 0). En revanche, un pod
normal redémarre continuellement, quel que soit son code de sortie. Les
jobs sont utiles pour les choses que vous ne voulez faire qu’une seule fois,
comme les migrations de bases de données ou les traitements par lots. Si
elle était gérée dans un pod normal, la tâche de migration d’une base de
données s’exécuterait en boucle, en repeuplant continuellement la base de
données après chaque sortie.
Dans ce chapitre, nous explorons les modèles de jobs les plus courants
proposés par Kubernetes. Nous allons également tirer parti de ces modèles
dans des scénarios s’appliquant à des situations réelles.

_ 10.1 L’OBJET JOB


L’objet Job est responsable de la création et de la gestion des pods définis
dans un modèle de spécification de job. Ces pods fonctionnent
généralement jusqu’à ce qu’ils se terminent avec succès. L’objet Job
coordonne l’exécution d’un certain nombre de pods en parallèle.
Si le pod a une défaillance avant qu’il ne termine sa tâche avec succès, le
contrôleur de jobs va créer un nouveau pod basé sur le modèle de pod de la
spécification du job. Étant donné que les pods doivent être ordonnancés, il y
a un risque que votre job ne s’exécute pas si l’ordonnanceur ne trouve pas
les ressources nécessaires. En outre, en raison de la nature des systèmes
distribués, il y a un risque minime, lors de certains scénarios de pannes, que
des pods soient créés en double pour une tâche spécifique.

_ 10.2 MODÈLES DE JOBS


Les jobs sont conçus pour gérer des charges de travail du genre des batchs,
où les éléments de travail sont traités par un ou plusieurs pods. Par défaut,
chaque job exécute un pod unique une seule fois jusqu’à ce qu’il se termine
avec succès. Ce modèle de job est défini par deux attributs principaux d’un
job, à savoir le nombre d’achèvements de jobs et le nombre de pods à
exécuter en parallèle. Dans le cas du modèle « exécuter une fois jusqu’à
l’achèvement », les paramètres completions et parallelism sont définis à 1.
Le tableau 10.1 illustre quelques modèles de jobs basés sur la combinaison
de paramètres completions et parallelism pour une configuration de job.

Tableau 10.1 – Modèles de jobs

Cas
Type Comportement completions parallelisme
d’utilisation

Un seul pod
fonctionnant
Migration de
une seule fois
Ponctuel bases de 1 1
jusqu’à ce qu’il
données
se termine avec
succès
Cas
Type Comportement completions parallelisme
d’utilisation

Un ou plusieurs
pods
Plusieurs
s’exécutant une
pods traitant
Achèvements ou plusieurs
un
fixes fois jusqu’à 1+ 1+
ensemble
parallèles atteindre un
de travaux
nombre
en parallèle
d’achèvements
fixe

Un ou plusieurs
Traitement
pods
File d’attente de plusieurs
fonctionnant
de travaux : pods à partir
une seule fois 1 2+
jobs d’une file de
jusqu’à ce qu’ils
parallèles travaux
se terminent
centralisée
avec succès

10.2.1 Jobs ponctuels

Les jobs ponctuels permettent d’exécuter un pod une seule fois jusqu’à ce
qu’il se termine avec succès. Bien qu’elle puisse paraître facile, cette tâche
nécessite un certain nombre d’opérations. Tout d’abord, un pod doit être créé
et soumis à l’API Kubernetes. Cela se fait à l’aide d’un modèle de pod défini
dans la configuration du job. Une fois qu’un job est en cours d’exécution, le
pod qui gère le job doit être surveillé car il faut contrôler qu’il se termine avec
succès. Il y a de multiples raisons pour qu’un job échoue, notamment une
erreur d’application, une exception non interceptée pendant l’exécution, ou
bien une panne de nœud avant que le job ne se termine. Dans tous les cas,
le contrôleur de jobs est responsable de la recréation du pod jusqu’à ce qu’il
se termine avec succès.
Il existe plusieurs façons de créer un job ponctuel dans Kubernetes. La plus
simple est d’utiliser l’outil en ligne de commande kubectl :
$ kubectl run -i oneshot \
--image=gcr.io/kuar-demo/kuard-amd64:1 \
--restart=OnFailure \
-- --keygen-enable \
--keygen-exit-on-complete \
--keygen-num-to-gen 10

...
(ID 0) Workload starting
(ID 0 1/10) Item done: SHA256:nAsUsG54XoKRkJwyN+OShkUPKew3mwq7OCc
(ID 0 2/10) Item done: SHA256:HVKX1ANns6SgF/er1lyo+ZCdnB8geFGt0/8
(ID 0 3/10) Item done: SHA256:irjCLRov3mTT0P0JfsvUyhKRQ1TdGR8H1jg
(ID 0 4/10) Item done: SHA256:nbQAIVY/yrhmEGk3Ui2sAHuxb/o6mYO0qRk
(ID 0 5/10) Item done: SHA256:CCpBoXNlXOMQvR2v38yqimXGAa/w2Tym+aI
(ID 0 6/10) Item done: SHA256:wEY2TTIDz4ATjcr1iimxavCzZzNjRmbOQp8
(ID 0 7/10) Item done: SHA256:t3JSrCt7sQweBgqG5CrbMoBulwk4lfDWiTI
(ID 0 8/10) Item done: SHA256:E84/Vze7KKyjCh9OZh02MkXJGoty9PhaCec
(ID 0 9/10) Item done: SHA256:UOmYex79qqbI1MhcIfG4hDnGKonlsij2k3s
(ID 0 10/10) Item done: SHA256:WCR8wIGOFag84Bsa8f/9QHuKqF+0mEnCADY
(ID 0) Workload exiting

Il faut noter ici les choses suivantes :


L’option -i de kubectl indique qu’il s’agit d’une commande interactive.
kubectl attend la fin du job en cours d’exécution, puis affiche la sortie du
journal du premier (et en l’occurrence unique) pod du job.
--restart=OnFailure est l’option qui indique à kubectl de créer un objet Job.
Toutes les options préfixées par -- sont des arguments en ligne de
commande pour l’image du conteneur. Elles indiquent à notre serveur de
test (kuard) de générer 10 clés SSH de 4 096 bits, puis de quitter.
Votre sortie peut ne pas correspondre exactement à notre exemple. Il
arrive souvent que kubectl n’affiche pas les premières lignes de la sortie
avec l’option -i.
Une fois le job terminé, l’objet Job et le pod correspondant sont toujours
présents si bien que vous pouvez inspecter la sortie des logs. Notez que ce
job n’apparaîtra pas dans la commande kubectl get jobs, sauf si vous passez
le paramètre -a. Sans ce paramètre, kubectl masque les jobs terminés.
Supprimez le job avant de poursuivre :
$ kubectl delete jobs oneshot
L’autre option pour créer un travail ponctuel consiste à utiliser un fichier de
configuration, comme cela est illustré dans l’exemple 10-1.
Exemple 10-1. job-oneshot.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: oneshot
labels:
chapter: jobs
spec:
template:
metadata:
labels:
chapter: jobs
spec:
containers:
- name: kuard
image: gcr.io/kuar-demo/kuard-amd64:1
imagePullPolicy: Always
args:
- "--keygen-enable"
- "--keygen-exit-on-complete"
- "--keygen-num-to-gen=10"
restartPolicy: OnFailure

Soumettez le job en utilisant la commande kubectl apply :


$ kubectl apply -f job-oneshot.yaml
job "oneshot" created

Puis utilisez la commande kubectl describe pour obtenir des informations sur
le job :
$ kubectl describe jobs oneshot
Name: oneshot
Namespace: default
Image(s): gcr.io/kuar-demo/kuard-amd64:1
Selector: controller-uid=cf87484b-e664-11e6-8222-42010a8a007b
Parallelism: 1
Completions: 1
Start Time: Sun, 29 Jan 2017 12:52:13 -0800
Labels: Job=oneshot
Pods Statuses: 0 Running / 1 Succeeded / 0 Failed
No volumes.
Events:
... Reason Message
... ------ -------
... SuccessfulCreate Created pod: oneshot-4kfdt
Vous pouvez afficher les résultats du job en regardant les journaux du pod
qui a été créé :
$ kubectl logs oneshot-4kfdt
...
Serving on :8080
(ID 0) Workload starting
(ID 0 1/10) Item done: SHA256:+r6b4W81DbEjxMcD3LHjU+EIGnLEzbpxITKn8IqhkPI
(ID 0 2/10) Item done: SHA256:mzHewajaY1KA8VluSLOnNMk9fDE5zdn7vvBS5Ne8AxM
(ID 0 3/10) Item done: SHA256:TRtEQHfflJmwkqnNyGgQm/IvXNykSBIg8c03h0g3onE
(ID 0 4/10) Item done: SHA256:tSwPYH/J347il/mgqTxRRdeZcOazEtgZlA8A3/HWbro
(ID 0 5/10) Item done: SHA256:IP8XtguJ6GbWwLHqjKecVfdS96B17nnO21I/TNc1j9k
(ID 0 6/10) Item done: SHA256:ZfNxdQvuST/6ZzEVkyxdRG98p73c/5TM99SEbPeRWfc
(ID 0 7/10) Item done: SHA256:tH+CNl/IUl/HUuKdMsq2XEmDQ8oAvmhMO6Iwj8ZEOj0
(ID 0 8/10) Item done: SHA256:3GfsUaALVEHQcGNLBOu4Qd1zqqqJ8j738i5r+I5XwVI
(ID 0 9/10) Item done: SHA256:5wV4L/xEiHSJXwLUT2fHf0SCKM2g3XH3sVtNbgskCXw
(ID 0 10/10) Item done: SHA256:bPqqOonwSbjzLqe9ZuVRmZkz+DBjaNTZ9HwmQhbdWLI
(ID 0) Workload exiting

Félicitations, votre job a été exécuté avec succès !

Vous avez peut-être remarqué que nous n’avons pas spécifié


d’étiquettes lors de la création de l’objet Job. Comme pour les autres
contrôleurs (DaemonSet, ReplicaSets, déploiements, etc.) qui utilisent
des étiquettes pour identifier un ensemble de pods, des comportements
inattendus peuvent se produire si un pod est réutilisé par des objets.
Comme les jobs ont des limites de début et de fin, il est courant que les
utilisateurs en créent beaucoup. Cela rend la sélection des étiquettes
uniques plus difficile et plus critique. C’est pour cette raison que l’objet
Job sélectionne automatiquement une étiquette unique et l’utilise pour
identifier les pods qu’il crée. Dans les scénarios avancés (comme la
permutation d’un job en cours d’exécution sans suppression des pods
qu’il gère) les utilisateurs peuvent choisir de désactiver ce
comportement automatique et de spécifier manuellement les étiquettes
et les sélecteurs.

◆ Défaillance des pods


Nous venons de voir comment un job peut se terminer avec succès. Mais
que se passe-t-il s’il y a une défaillance ? Nous allons provoquer une panne
et voir ce qui se passe.
Modifions les arguments de kuard dans notre fichier de configuration pour le
faire planter avec un code de sortie non nul après avoir généré trois clés,
comme cela est illustré dans l’exemple 10-2.
Exemple 10-2. job-oneshot-failure1.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: oneshot
labels:
chapter: jobs
spec:
template:
metadata:
labels:
chapter: jobs
spec:
containers:
- name: kuard
image: gcr.io/kuar-demo/kuard-amd64:1
imagePullPolicy: Always
args:
- "--keygen-enable"
- "--keygen-exit-on-complete"
- "--keygen-exit-code=1"
- "--keygen-num-to-gen=3"
restartPolicy: OnFailure

À présent, exécutez la commande kubectl apply -f job-oneshot-failure1.yaml.


Attendez un peu, puis regardez le statut du pod :
$ kubectl get pod -a -l job-name=oneshot

NAME READY STATUS RESTARTS AGE

oneshot-3ddk0 0/1 CrashLoopBackOff 4 3m

Dans cet exemple, nous voyons que le même pod a redémarré quatre fois.
Kubernetes est dans l’état CrashLoopBackOff pour ce pod. Il n’est pas rare
d’avoir un bug quelque part qui provoque un plantage du programme dès
qu’il démarre. Dans ce cas, Kubernetes va attendre un peu avant de
redémarrer le pod, afin d’éviter des plantages en boucle qui consommeraient
les ressources du nœud. Tout cela est géré localement au niveau du nœud
par kubelet sans que le job ne soit impliqué.
Supprimons le job (kubectl delete jobs oneshot), et essayons autre chose.
Nous allons modifier à nouveau le fichier de configuration et faire passer le
paramètre restartPolicy de OnFailure à Never. Exécutez la commande
kubectl apply -f jobs-oneshot-failure2.yaml.
Si nous attendons un peu, puis examinons les pods liés au job, nous allons
trouver quelque chose d’intéressant :
$ kubectl get pod -l job-name=oneshot -a

oneshot-
NAME READY STATUS RESTARTS AGE 0/1
0wm49

Error 0 1m oneshot-6h9s2 0/1 Error 0 39s

oneshot- oneshot-
1/1 Running 0 6s 0/1
hkzw0 k5swz

28s oneshot-
Error 0 0/1 Error 0 19s
m1rdw

oneshot-
0/1 Error 0 57s
x157b

On peut constater qu’il y a plusieurs pods qui ont des erreurs.


En définissant restartPolicy à Never, nous indiquons à kubelet de ne pas
redémarrer le pod en cas de panne, mais au contraire de simplement
déclarer le pod comme défaillant. Ensuite, l’objet Job est averti et il crée un
pod de remplacement. Si vous n’êtes pas prudent, cela va créer beaucoup
de « déchets » dans votre cluster. C’est pour cette raison que nous vous
suggérons de définir restartPolicy à OnFailure pour que les pods défaillants
soient réexécutés et qu’il n’y ait pas de nouvelle création de pods.
Faites le ménage avec la commande kubectl delete jobs oneshot.
Jusqu’à présent, nous avons vu qu’un programme se plante en quittant avec
un code de sortie non nul. Mais les workers peuvent se planter selon
d’autres modalités. Plus précisément, ils peuvent se bloquer et devenir
inactifs. Pour traiter ce genre de cas, vous pouvez utiliser des sondes
d’activité avec des jobs. Si la stratégie de la sonde d’activité détermine qu’un
pod est mort, il sera redémarré ou remplacé automatiquement.
10.2.2 Parallélisme

La génération des clés peut être lente. Nous allons démarrer ensemble un
groupe de workers afin de rendre la génération de clés plus rapide. Nous
allons utiliser une combinaison de paramètres completions et parallelism.
Notre objectif est de générer 100 clés en exécutant 10 kuard générant
chacun 10 clés. Mais comme nous ne voulons pas surcharger notre cluster,
nous allons nous limiter à cinq pods à la fois.
Cela se traduit par le paramètre completions défini à 10 et parallelism à 5. La
configuration est illustrée dans l’exemple 10-3.
Exemple 10-3. job-parallel.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: parallel
labels:
chapter: jobs
spec:
parallelism: 5
completions: 10
template:
metadata:
labels:
chapter: jobs
spec:
containers:
- name: kuard
image: gcr.io/kuar-demo/kuard-amd64:1
imagePullPolicy: Always
args:
- "--keygen-enable"
- "--keygen-exit-on-complete"
- "--keygen-num-to-gen=10"
restartPolicy: OnFailure

Exécutez la commande suivante :


$ kubectl apply -f job-parallel.yaml
job "parallel" created

Examinez à présent la manière dont les pods sont créés, agissent et se


terminent. De nouveaux pods sont créés jusqu’à ce qu’il y en ait une dizaine.
Ici, nous utilisons le paramètre --watch pour que kubectl reste actif et liste les
changements au fur et à mesure qu’ils se produisent :
$ kubectl get pods -w

NAME READY STATUS RESTARTS AGE

parallel-55tlv 1/1 Running 0 5s

parallel-5s7s9 1/1 Running 0 5s

parallel-jp7bj 1/1 Running 0 5s

parallel-lssmn 1/1 Running 0 5s

parallel-qxcxp 1/1 Running 0 5s

NAME READY STATUS RESTARTS AGE

parallel-jp7bj 0/1 Completed 0 26s

parallel-tzp9n 0/1 Pending 0 0s

parallel-tzp9n 0/1 Pending 0 0s

parallel-tzp9n 0/1 ContainerCreating 0 1s

parallel-tzp9n 1/1 Running 0 1s

parallel-tzp9n 0/1 Completed 0 48s

parallel-x1kmr 0/1 Pending 0 0s

parallel-x1kmr 0/1 Pending 0 0s

parallel-x1kmr 0/1 ContainerCreating 0 0s

parallel-x1kmr 1/1 Running 0 1s

parallel-5s7s9 0/1 Completed 0 1m

parallel-tprfj 0/1 Pending 0 0s

parallel-tprfj 0/1 Pending 0 0s


parallel-tprfj 0/1 ContainerCreating 0 0s

parallel-tprfj 1/1 Running 0 2s

parallel-x1kmr 0/1 Completed 0 52s

parallel-bgvz5 0/1 Pending 0 0s

parallel-bgvz5 0/1 Pending 0 0s

parallel-bgvz5 0/1 ContainerCreating 0 0s

parallel-bgvz5 1/1 Running 0 2s

parallel-qxcxp 0/1 Completed 0 2m

parallel-xplw2 0/1 Pending 0 1s

parallel-xplw2 0/1 Pending 0 1s

parallel-xplw2 0/1 ContainerCreating 0 1s

parallel-xplw2 1/1 Running 0 3s

parallel-bgvz5 0/1 Completed 0 40s

parallel-55tlv 0/1 Completed 0 2m

parallel-lssmn 0/1 Completed 0 2m

N’hésitez pas à fouiner dans les jobs terminés et à vérifier leurs logs pour
voir les empreintes des clés qu’ils ont générées. Faites le ménage en
supprimant l’objet Job, une fois qu’il est terminé, grâce à la commande
kubectl delete job parallel.

10.2.3 Files d’attente de travaux

Il est courant que les jobs traitent le travail à partir d’une file d’attente de
travaux. Dans ce scénario, une tâche crée un certain nombre d’éléments de
travail et les publie dans une file d’attente de travaux. Un job worker peut
être exécuté pour traiter chaque élément de travail jusqu’à ce que la file
d’attente de travaux soit vide (figure 10.1).
Figure 10.1 – Jobs parallèles.

◆ Démarrage d’une file d’attente de travaux

On commence par lancer un service de file d’attente de travaux centralisé.


kuard dispose d’un système simple et intégré de file d’attente de travaux
basé sur la mémoire. Nous allons démarrer une instance de kuard qui agira
en tant que coordonnateur pour tous les travaux à réaliser.
Créez un ReplicaSet simple pour gérer un démon de file d’attente de travaux
unique. Nous utilisons un ReplicaSet pour nous assurer qu’un nouveau pod
sera créé en cas de panne de la machine, comme cela est illustré dans
l’exemple 10-4.
Exemple 10-4. rs-queue.yaml
apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
labels:
app: work-queue
component: queue
chapter: jobs
name: queue
spec:
replicas: 1
template:
metadata:
labels:
app: work-queue
component: queue
chapter: jobs
spec:
containers:
- name: queue
image: "gcr.io/kuar-demo/kuard-amd64:1"
imagePullPolicy: Always
Exécutez la file d’attente de travaux avec la commande suivante :
$ kubectl apply -f rs-queue.yaml

À ce stade, le démon de la file d’attente de travaux doit être opérationnel.


Nous allons utiliser une redirection de port pour nous y connecter. Exécutez
cette commande dans une fenêtre de terminal et laissez-la tourner :
$ QUEUE_POD=$(kubectl get pods -l app=work-queue,component=queue \
-o jsonpath='{.items[0].metadata.name}')
$ kubectl port-forward $QUEUE_POD 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

Vous pouvez ouvrir votre navigateur à l’adresse http://localhost:8080 et


visualiser l’interface de kuard. Passez à l’onglet « MemQ Server » pour
garder un œil sur ce qui se passe.
Une fois le serveur de file d’attente de travaux opérationnel, nous devons
l’exposer à l’aide d’un service. Cela facilitera la tâche des producteurs et des
consommateurs qui voudront localiser la file d’attente de travaux via le DNS,
comme cela est illustré dans l’exemple 10-5.
Exemple 10-5. service-queue.yaml
apiVersion: v1
kind: Service
metadata:
labels:
app: work-queue
component: queue
chapter: jobs
name: queue
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
app: work-queue
component: queue

Créez le service de file d’attente avec la commande kubectl suivante :


$ kubectl apply -f service-queue.yaml
service "queue" created
◆ Chargement de la file d’attente

Nous sommes maintenant prêts à mettre une série d’éléments de travail


dans la file d’attente. Par souci de simplicité, nous allons nous contenter
d’utiliser curl pour piloter l’API du serveur de file d’attente de travaux et
insérer un groupe d’éléments de travail. curl communiquera à la file d’attente
de travaux la redirection de port que nous avons définie plus haut, comme
cela est illustré dans l’exemple 10-6.
Exemple 10-6. load-queue.sh
# Crée une file d’attente de travaux appelée 'keygen'
curl -X PUT localhost:8080/memq/server/queues/keygen

# Crée 100 éléments de travail et les charge dans la file.


for i in work-item-{0..99}; do
curl -X POST localhost:8080/memq/server/queues/keygen/enqueue \
-d "$i"
done

Exécutez ces commandes et vous devriez voir sur la sortie de votre terminal
une centaine d’objets JSON avec un identificateur de message unique pour
chaque élément de travail. Vous pouvez vérifier l’état de la file d’attente en
regardant l’onglet « MemQ Server » dans l’interface utilisateur, ou vous
pouvez demander directement à l’API de la file d’attente de travaux :
$ curl 127.0.0.1:8080/memq/server/stats
{
"kind": "stats",
"queues": [
{
"depth": 100,
"dequeued": 0,
"drained": 0,
"enqueued": 100,
"name": "keygen"
}
]
}

Maintenant, nous sommes prêts à lancer un job pour consommer la file


d’attente de travaux jusqu’à ce qu’elle soit vide.
◆ Création du job des consommateurs

C’est là que ça devient intéressant ! kuard est également capable d’agir en


mode consommateur. Dans l’exemple 10-7, nous l’avons configuré pour
extraire des éléments de travail à partir de la file d’attente de travaux, créer
une clé, puis quitter une fois que la file d’attente est vide.
Exemple 10-7. job-consumers.yaml
apiVersion: batch/v1
kind: Job
metadata:
labels:
app: message-queue
component: consumer
chapter: jobs
name: consumers
spec:
parallelism: 5
template:
metadata:
labels:
app: message-queue
component: consumer
chapter: jobs
spec:
containers:
- name: worker
image: "gcr.io/kuar-demo/kuard-amd64:1"
imagePullPolicy: Always
args:
- "--keygen-enable"
- "--keygen-exit-on-complete"
- "--keygen-memq-server=http://queue:8080/memq/server"
- "--keygen-memq-queue=keygen"
restartPolicy: OnFailure

Nous disons au job de lancer cinq pods en parallèle. Comme le paramètre


completions est désactivé, nous avons mis le job dans un mode de pool de
workers. Une fois que le premier pod quitte avec un code de sortie zéro, le
job commence à ralentir et ne démarre pas de nouveaux pods. Cela signifie
qu’aucun des workers ne doit quitter tant que le travail n’est pas terminé et
qu’ils sont tous dans un processus d’achèvement.
Créez le job consumers avec la commande suivante :
$ kubectl apply -f job-consumers.yaml
job "consumers" created

Une fois que le job a été créé, vous pouvez voir les pods qui supportent le
job :
$ kubectl get pods

NAME READY STATUS RESTARTS AGE

queue-43s87 1/1 Running 0 5m

consumers-6wjxc 1/1 Running 0 2m

consumers-7l5mh 1/1 Running 0 2m

consumers-hvz42 1/1 Running 0 2m

consumers-pc8hr 1/1 Running 0 2m

consumers-w20cc 1/1 Running 0 2m

Vous noterez qu’il y a cinq pods qui s’exécutent en parallèle. Ces pods
continueront à s’exécuter jusqu’à ce que la file d’attente de travaux soit vide.
Vous pouvez observer quand cela se produit dans l’interface utilisateur sur le
serveur de la file d’attente de travaux. Au fur et à mesure que la file d’attente
se vide, les pods des consommateurs quitteront et le job consumers sera
considéré comme étant achevé.

◆ Nettoyage

En utilisant des étiquettes, on peut supprimer toutes les choses que nous
avons créées dans cette section :
$ kubectl delete rs,svc,job -l chapter=jobs

_ 10.3 RÉSUMÉ
Sur un cluster unique, Kubernetes peut gérer à la fois les charges de travail
qui durent longtemps, comme les applications Web, et les charges de travail
de courte durée, comme les traitements par lots. L’abstraction Job vous
permet de modéliser des modèles de travail par lots qui vont des tâches
simples ponctuelles aux tâches parallèles traitant de nombreux éléments
jusqu’à achèvement du travail.
Les jobs sont une primitive de bas niveau qui peut être utilisée directement
pour des charges de travail simples. Toutefois, Kubernetes est conçu à la
base pour être extensible grâce à des objets de niveau supérieur. Les jobs
ne font pas exception à cette règle et ils peuvent facilement être utilisés par
les systèmes d’orchestration de niveau supérieur pour gérer des tâches plus
complexes.
11

ConfigMaps et secrets

C’est une bonne pratique de rendre les images de conteneurs aussi


réutilisables que possible. La même image doit pouvoir être utilisée
pour le développement, la simulation et la production. C’est encore
mieux si la même image est suffisamment générique pour être
employée par les applications et les services. Les tests et le contrôle
des versions sont plus risqués et plus complexes si les images
doivent être recréées pour chaque nouvel environnement. Mais dans
ces conditions, comment faire pour personnaliser l’utilisation de cette
image au moment de l’exécution ?
C’est là que les ConfigMaps et les secrets entrent en jeu. Les
ConfigMaps servent à fournir des informations de configuration pour
les charges de travail. Il peut s’agir d’informations précises (une
courte chaîne de caractères) ou d’une valeur composite sous la
forme d’un fichier. Les secrets sont semblables aux ConfigMaps,
mais ils sont focalisés sur la fourniture d’informations sensibles à la
charge de travail. Ils peuvent être utilisés pour les informations
d’identification ou les certificats TLS.

_ 11.1 CONFIGMAPS
On peut se représenter un ConfigMap comme un objet Kubernetes
qui définit un petit système de fichiers. On peut aussi se le
représenter comme un ensemble de variables qui peuvent être
employées quand on définit l’environnement ou la ligne de
commande pour les conteneurs. La chose principale à retenir est
que le ConfigMap est combiné avec le pod juste avant qu’il ne soit
exécuté. Cela signifie que l’image du conteneur et la définition du
pod peuvent être réutilisées à travers de nombreuses applications
en changeant simplement le ConfigMap qui est employé.

11.1.1 Création de ConfigMaps

Commençons tout de suite par créer un ConfigMap. Comme pour de


nombreux objets dans Kubernetes, vous pouvez les créer d’une
manière immédiate à l’aide d’une commande, ou vous pouvez les
créer à partir d’un manifeste stocké sur le disque. Commençons par
la méthode impérative.
Tout d’abord, supposons que nous ayons un fichier sur le disque
(appelé my-config.txt) que nous voulons mettre à la disposition du
pod en question, comme cela est illustré dans l’exemple 11-1.
Exemple 11-1. my-config.txt
# Exemple de fichier de configuration qui peut être utilisé pour configurer une
application
parameter1 = value1
parameter2 = value2

Ensuite, nous allons créer un ConfigMap avec ce fichier. Nous allons


également ajouter ici quelques paires clé/valeur simples qui sont
appelées sous la forme de valeurs littérales sur la ligne de
commande :
$ kubectl create configmap my-config \
--from-file=my-config.txt \
--from-literal=extra-param=extra-value \
--from-literal=another-param=another-value

Voici le fichier YAML équivalent pour l’objet ConfigMap que nous


venons de créer :
$ kubectl get configmaps my-config -o yaml
apiVersion: v1
data:
another-param: another-value
extra-param: extra-value
my-config.txt: |
# Exemple de fichier de configuration servant à configurer une application
parameter1 = value1
parameter2 = value2
kind: ConfigMap
metadata:
creationTimestamp: ...
name: my-config
namespace: default
resourceVersion: "13556"
selfLink: /api/v1/namespaces/default/configmaps/my-config
uid: 3641c553-f7de-11e6-98c9-06135271a273

Comme vous pouvez le voir, le ConfigMap n’est vraiment constitué


que de quelques paires clé/valeur stockées dans un objet. Les
choses intéressantes commencent lorsque vous essayez d’utiliser
un ConfigMap.

11.1.2 Utilisation d’un ConfigMap

Il existe trois façons principales d’utiliser un ConfigMap :


Système de fichiers
Vous pouvez monter un ConfigMap dans un pod. Un fichier est
créé pour chaque entrée en se basant sur le nom de la clé. Le
contenu de ce fichier est défini avec la valeur de la paire
clé/valeur.
Variable d’environnement
Un ConfigMap peut être utilisé pour définir de manière
dynamique la valeur d’une variable d’environnement.
Argument de ligne de commande
Kubernetes prend en charge la création dynamique de la ligne
de commande d’un conteneur en se basant sur les valeurs d’un
ConfigMap.
Créons à présent un manifeste pour kuard qui rassemble tout ceci :
Exemple 11-2. kuard-config.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard-config
spec:
containers:
- name: test-container
image: gcr.io/kuar-demo/kuard-amd64:1
imagePullPolicy: Always
command:
- "/kuard"
- "$(EXTRA_PARAM)"
env:
- name: ANOTHER_PARAM
valueFrom:
configMapKeyRef:
name: my-config
key: another-param
- name: EXTRA_PARAM
valueFrom:
configMapKeyRef:
name: my-config
key: extra-param
volumeMounts:
- name: config-volume
mountPath: /config
volumes:
- name: config-volume
configMap:
name: my-config
restartPolicy: Never

Pour la méthode du système de fichiers, nous créons un nouveau


volume à l’intérieur du pod que nous nommons config-volume. Nous
définissons ensuite ce volume comme étant un volume ConfigMap et
nous pointons sur le ConfigMap à monter. Nous devons préciser
l’emplacement du montage dans le conteneur kuard avec un
volumeMount. Dans ce cas, l’emplacement du montage est /config.
Les variables d’environnement sont spécifiées avec un membre
spécial valueFrom qui fait référence au ConfigMap et à la clé de
données à utiliser dans ce ConfigMap.
Les arguments de ligne de commande s’appuient sur des variables
d’environnement. Kubernetes effectuera la substitution correcte avec
une syntaxe spéciale $(<nom-variable-environnement>).
Nous allons exécuter ce pod et faire une redirection de port afin
d’examiner comment l’application voit le monde :
$ kubectl apply -f kuard-config.yaml
$ kubectl port-forward kuard-config 8080

Pointez à présent votre navigateur à http://localhost:8080. Nous


pouvons examiner comment nous avons injecté des valeurs de
configuration dans le programme en employant les trois façons
différentes que nous avons décrites.
Cliquez sur l’onglet « Server Env » sur la gauche et vous verrez la
ligne de commande avec laquelle l’application a été lancée ainsi que
son environnement (figure 11.1).

Figure 11.1 – Affichage de l’environnement de kuard.


Ici, nous pouvons voir que nous avons ajouté deux variables
d’environnement (ANOTHER_PARAM et EXTRA_PARAM) dont les
valeurs sont définies via le ConfigMap. En outre, nous avons ajouté
un argument à la ligne de commande de kuard en nous basant sur la
valeur de EXTRA_PARAM.
Cliquez ensuite sur l’onglet « File system browser » (figure 11.2).
Cela vous permet d’explorer le système de fichiers de la même
manière qu’il est vu par l’application. Vous devriez voir une entrée
appelée /config. Il s’agit d’un volume créé sur la base de notre
ConfigMap. Si vous naviguez dans ce dernier, vous constaterez
qu’un fichier a été créé pour chaque entrée du ConfigMap. Vous
verrez également quelques fichiers cachés (préfixés par ..) qui sont
utilisés pour faire un échange propre des nouvelles valeurs lorsque
le ConfigMap est mis à jour.

Figure 12.1 – Répertoire /config tel qu’il est vu par kuard.


_ 11.2 SECRETS
Alors que les ConfigMaps sont parfaits pour la plupart des données
de configuration, il y a certaines données qui sont d’une extrême
sensibilité, notamment les mots de passe, les jetons de sécurité ou
d’autres types de clés privées. Nous rassemblons tous ces types de
données sous le nom de « secrets ». Kubernetes prend en charge
nativement le stockage et la gestion de ces données de façon
appropriée.
Les secrets permettent de créer des images de conteneurs sans
regrouper les données sensibles. Cela permet aux conteneurs de
rester portables à travers différents environnements. Les secrets
sont exposés aux pods via une déclaration explicite dans les
manifestes de pods et l’API Kubernetes. De cette façon, l’API des
secrets de Kubernetes fournit un mécanisme centré sur l’application
pour exposer des informations de configuration sensibles aux
applications d’une manière qui est facile à auditer et qui exploite les
primitives d’isolation des systèmes d’exploitation natifs.

En fonction de vos besoins, il est possible que les secrets


de Kubernetes ne soient pas assez sûrs. Depuis la version 1.6
de Kubernetes, toute personne ayant un accès root sur
n’importe quel nœud a accès à tous les secrets du cluster. Bien
que Kubernetes utilise les primitives de conteneurisation des
systèmes d’exploitation natifs pour exposer uniquement les
pods aux secrets qu’ils sont censés voir, l’isolation entre les
nœuds est toujours un chantier en cours.
La version 1.7 de Kubernetes améliore un peu la situation.
Lorsque Kubernetes est correctement configuré, il crypte les
secrets stockés et limite les secrets auxquels chaque nœud
individuel a accès.

Le reste de cette section va explorer la manière de créer et de gérer


les secrets Kubernetes, et aussi présenter les meilleures pratiques
pour exposer les secrets aux pods qui en ont besoin.

11.2.1 Création des secrets

Les secrets sont créés à l’aide de l’API Kubernetes ou de l’outil en


ligne de commande kubectl. Les secrets contiennent un ou plusieurs
éléments de données sous la forme d’une collection de paires
clé/valeur.
Dans cette section, nous allons créer un secret pour stocker une clé
et un certificat TLS pour l’application kuard qui satisfait aux
exigences de stockage énumérées ci-dessus.
L’image du conteneur kuard ne regroupe pas de certificat
ou de clé TLS. Cela permet au conteneur kuard de rester
portable entre les différents environnements et distribuable via
des dépôts publics Docker.

La première étape de la création d’un secret est d’obtenir les


données brutes que l’on veut stocker. La clé et le certificat TLS pour
l’application kuard peuvent être téléchargés en exécutant les
commandes suivantes (veuillez ne pas utiliser ces certificats en
dehors de cet exemple) :
$ curl -o kuard.crt https://storage.googleapis.com/kuar-demo/kuard.crt
$ curl -o kuard.key https://storage.googleapis.com/kuar-demo/kuard.key

Quand les fichiers kuard.crt et kuard.key sont stockés localement,


nous sommes prêts à créer un secret. On crée un secret nommé
kuard-tls en utilisant la commande create secret :
$ kubectl create secret generic kuard-tls \
--from-file=kuard.crt \
--from-file=kuard.key

Le secret kuard-tls a été créé avec deux éléments de données.


Exécutez la commande suivante pour obtenir des détails :
$ kubectl describe secrets kuard-tls
Name: kuard-tls
Namespace: default
Labels: <none>
Annotations: <none>

Type: Opaque

Data
====
kuard.crt: 1050 bytes
kuard.key: 1679 bytes
Le secret kuard-tls en place, nous pouvons l’utiliser à partir d’un pod
en utilisant un volume de secrets.

11.2.2 Utilisation des secrets

Les secrets peuvent être employés à l’aide de l’API REST


Kubernetes par les applications qui savent comment appeler cette
API directement. Cependant, notre objectif est de garder les
applications portables. Non seulement elles doivent s’exécuter
correctement dans Kubernetes, mais elles doivent aussi s’exécuter
sans modification sur d’autres plateformes.
Au lieu d’accéder aux secrets via le serveur API, nous pouvons
utiliser un volume de secrets.

◆ Volumes de secrets

Les données secrètes peuvent être exposées aux pods en utilisant


le type volume de secrets. Les volumes de secrets sont gérés par le
kubelet et sont générés au moment de la création du pod. Les
secrets sont stockés sur des volumes tmpfs (c’est-à-dire des disques
RAM) et, en tant que tels, ils ne sont pas écrits sur un disque d’un
nœud.
Chaque élément de données d’un secret est stocké dans un fichier
séparé sous le point de montage cible spécifié dans le montage du
volume. Le secret kuard-tls contient deux éléments de données :
kuard.crt et kuard.key. Le montage du volume des secrets kuard-tls
sur /tls donne lieu aux fichiers suivants :
/tls/kuard.crt
/tls/kuard.key

Le manifeste de pod suivant (exemple 11-3) montre comment


déclarer un volume des secrets, qui expose le secret kuard-tls au
conteneur kuard sous /tls.
Exemple 11-3. kuard-secret.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard-tls
spec:
containers:
- name: kuard-tls
image: gcr.io/kuar-demo/kuard-amd64:1
imagePullPolicy: Always
volumeMounts:
- name: tls-certs
mountPath: "/tls"
readOnly: true
volumes:
- name: tls-certs
secret:
secretName: kuard-tls

Créez le pod kuard-tls en utilisant kubectl et observez la sortie des


logs du pod en cours d’exécution :
$ kubectl apply -f kuard-secret.yaml

Connectez-vous au pod en exécutant :


$ kubectl port-forward kuard-tls 8443:8443

Pointez à présent votre navigateur à https://localhost:8443. Vous


devriez voir quelques avertissements de certificat non valides car il
s’agit d’un certificat auto-signé pour kuard.example.com. Si vous
ignorez cet avertissement, vous devriez voir le serveur kuard
hébergé via HTTPS. Utilisez l’onglet « File system browser » pour
trouver les certificats sur le disque.

11.2.3 Registres privés Docker

On se sert tout particulièrement des secrets pour stocker les


informations d’identification d’accès aux registres privés Docker.
Kubernetes prend en charge l’utilisation d’images stockées sur des
registres privés, mais l’accès à ces images requiert des informations
d’identification. Les images privées peuvent être stockées dans un
ou plusieurs registres privés. La gestion des informations
d’identification de chaque registre privé sur chaque nœud potentiel
du cluster est très délicate.
Les secrets d’extraction d’image (image pull secrets) exploitent l’API
des secrets pour automatiser la distribution des informations
d’identification du registre privé. Les secrets d’extraction d’image
sont stockés exactement comme les secrets normaux, mais ils sont
utilisés par le champ spec.imagePullSecrets de la spécification du
pod.
Utilisez la commande create secret docker-registry pour créer ce
type particulier de secret :
$ kubectl create secret docker-registry my-image-pull-secret \
--docker-username=<username> \
--docker-password=<password> \
--docker-email=<email-address>

Autorisez l’accès au dépôt privé en référençant le secret d’extraction


d’image dans le fichier de manifeste de pod, comme cela est illustré
dans l’exemple 11-4.
Exemple 11-4. kuard-secret-ips.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard-tls
spec:
containers:
- name: kuard-tls
image: gcr.io/kuar-demo/kuard-amd64:1
imagePullPolicy: Always
volumeMounts:
- name: tls-certs
mountPath: "/tls"
readOnly: true
imagePullSecrets:
- name: my-image-pull-secret
volumes:
- name: tls-certs
secret:
secretName: kuard-tls

_ 11.3 CONTRAINTES DE NOMMAGE


Les noms des clés des éléments de données à l’intérieur d’un secret
ou d’un ConfigMap sont définis pour correspondre à des noms de
variables d’environnement valides. Ils peuvent commencer par un
point suivi d’une lettre ou d’un chiffre. Les caractères suivants
peuvent inclure des points, des tirets et des barres de soulignement.
Les points ne peuvent pas être répétés, et les points, les barres de
soulignement ou les tirets ne peuvent pas être adjacents. Plus
formellement, cela signifie qu’ils doivent se conformer à l’expression
régulière [.]?[a-zAZ0-9]([.]?[-_a-zA-Z0-9]*[a-zA-Z0-9])*. Le
tableau 11.1 liste quelques exemples de noms valides et invalides
pour les ConfigMaps ou les secrets.

Tableau 11.1 – Exemples de clés pour


des ConfigMaps et des secrets.

Nom de clé valide Nom de clé invalide

.auth_token Token..properties

Key.pem auth file.json

config_file _password.txt
Lors de la sélection d’un nom de clé, vous devez prendre
en compte le fait que ces clés peuvent être exposées à des
pods via un montage de volume. Choisissez un nom qui sera
significatif lorsqu’il sera spécifié sur une ligne de commande ou
dans un fichier de configuration. Le stockage d’une clé TLS
sous la forme key.pem est plus clair que tls-key lors de la
configuration des applications pour accéder aux secrets.

Les valeurs des données des ConfigMap sont un simple texte au


format UTF-8 spécifié directement dans le manifeste. À partir de la
version 1.6 de Kubernetes, les ConfigMaps sont incapables de
stocker des données binaires.
Les valeurs des données secrètes contiennent des données
arbitraires codées en base64. L’utilisation de l’encodage base64
permet de stocker des données binaires. Cela étant, il est cependant
plus difficile de gérer les secrets qui sont stockés dans les fichiers
YAML car les valeurs codées en base64 doivent être inscrites dans
le fichier YAML.

_ 11.4 GESTION DES CONFIGMAPS


ET DES SECRETS
Les secrets et les ConfigMaps sont gérés par l’intermédiaire de l’API
Kubernetes. Les commandes habituelles create, delete, get et
describe fonctionnent pour la manipulation de ces objets.

11.4.1 Liste

Vous pouvez utiliser la commande kubectl get secrets pour


répertorier tous les secrets dans l’espace de noms actuel :
$ kubectl get secrets

NAME TYPE DATA AGE

default-token- kubernetes.io/service-account-
3 1h
f5jq2 token

kuard-tls Opaque 2 20m

De la même manière, vous pouvez répertorier tous les ConfigMaps


dans un espace de noms :
$ kubectl get configmaps

NAME DATA AGE

my-config 3 1m

kubectl describe peut être utilisé pour obtenir plus de détails sur un
objet particulier :
$ kubectl describe configmap my-config
Name: my-config
Namespace: default
Labels: <none>
Annotations: <none>

Data
====
another-param: 13 bytes
extra-param: 11 bytes
my-config.txt: 116 bytes

Enfin, vous pouvez voir les données brutes (y compris les valeurs
dans les secrets…) avec une commande similaire à kubectl get
configmap my-config -o yaml ou kubectl get secret kuard-tls-o yaml.

11.4.2 Création
La façon la plus simple de créer un secret ou un ConfigMap consiste
à exécuter une commande kubectl create secret generic ou kubectl
create configmap. Il existe une grande variété de façons de spécifier
les éléments de données qui sont stockés dans le secret ou le
ConfigMap. On peut combiner les différentes options en une seule
commande :
--from-file=<nom-de-fichier>

Charge à partir du fichier ayant le même nom que la clé des


données secrètes.
--from-file=<key>=<nom-de-fichier>

Charge à partir du fichier dont le nom est explicitement spécifié.


--from-file=<répertoire>

Charge à partir du répertoire spécifié tous les fichiers dont le nom


est un nom de clé acceptable.
--from-literal=<clé>=<valeur>

Utilise directement la paire clé/valeur spécifiée.

11.4.3 Mise à jour

Vous pouvez mettre à jour un ConfigMap ou un secret et faire


prendre en compte cette modification dans les programmes en cours
d’exécution. Il n’est pas nécessaire de redémarrer si l’application est
paramétrée pour relire les valeurs de la configuration. Il s’agit d’une
fonctionnalité rare, mais vous en aurez peut-être besoin dans vos
propres applications.
Voici trois façons de mettre à jour des ConfigMaps ou des secrets.

◆ Mise à jour à partir d’un fichier

Si vous avez un manifeste pour votre ConfigMap ou votre secret,


vous pouvez simplement le modifier directement et le remplacer par
une nouvelle version avec kubectl replace -f <nom-de-fichier>. Vous
pouvez également utiliser kubectl apply -f <nom-de-fichier> si vous
avez déjà créé la ressource avec kubectl apply.
En raison de la façon dont les fichiers sont encodés dans ces objets,
la mise à jour d’une configuration peut se révéler un peu fastidieuse
car il n’y a aucune disposition dans kubectl pour charger des
données à partir d’un fichier externe. Les données doivent être
stockées directement dans le manifeste YAML.
Le cas d’utilisation le plus fréquent est lorsque le ConfigMap est
défini comme faisant partie d’un répertoire ou d’une liste de
ressources et que tout est créé et mis à jour ensemble. Souvent, ces
manifestes seront vérifiés dans le contrôle de code source.

C’est généralement une mauvaise idée de vérifier les


fichiers YAML des secrets dans le contrôle de code source car il
est extrêmement facile de déplacer par inadvertance ces
fichiers dans un emplacement public et de publier ainsi vos
secrets.

◆ Recréation et mise à jour

Si vous stockez les entrées dans votre ConfigMaps ou des secrets


sous la forme de fichiers distincts sur le disque (par opposition à une
incorporation directe dans des fichiers YAML), vous pouvez utiliser
kubectl pour recréer le manifeste, puis l’utiliser pour mettre à jour
l’objet.
Pour ce faire, vous pouvez exécuter une commande similaire à ce
qui suit :
$ kubectl create secret generic kuard-tls \
--from-file=kuard.crt --from-file=kuard.key \
--dry-run -o yaml | kubectl replace -f -

Cette ligne de commande crée d’abord un nouveau secret du même


nom que le secret qui existe déjà. Si nous nous arrêtons là, le
serveur API Kubernetes retournera une erreur et se plaindra que
nous essayons de créer un secret qui existe déjà. Au lieu de cela,
nous disons à kubectl de ne pas réellement envoyer les données sur
le serveur, mais plutôt d’écrire sur stdout le fichier YAML qu’il aurait
envoyé au serveur API. On utilise ensuite un pipe (|) avec kubectl
replace et on emploie -f pour lui dire de lire à partir de stdin. De cette
façon, nous pouvons mettre à jour un secret à partir de fichiers sur le
disque sans avoir à coder à la main en base64 des données.

◆ Modification de la version actuelle

La dernière façon de mettre à jour un ConfigMap consiste à utiliser


kubectl edit pour charger dans votre éditeur une version du
ConfigMap afin de pouvoir l’optimiser (vous pouvez aussi le faire
avec un secret, mais vous serez coincé par la gestion de l’encodage
en base64 de vos valeurs) :
$ kubectl edit configmap my-config

Vous devriez voir la définition du ConfigMap dans votre éditeur.


Effectuez les modifications que vous souhaitez, puis enregistrez et
fermez votre éditeur. La nouvelle version de l’objet sera publiée sur
le serveur API Kubernetes.

◆ Mises à jour en direct

Une fois qu’un ConfigMap ou qu’un secret est mis à jour à l’aide de
l’API, il sera automatiquement publié sur tous les volumes qui
utilisent ce ConfigMap ou ce secret. Cela peut prendre quelques
secondes, mais la liste des fichiers et le contenu des fichiers, tels
qu’ils sont vus par kuard, seront mis à jour avec ces nouvelles
valeurs. En utilisant cette fonctionnalité de mise à jour en direct,
vous pouvez mettre à jour la configuration des applications sans les
redémarrer.
Actuellement, il n’existe aucun moyen intégré pour signaler à une
application qu’une nouvelle version d’un ConfigMap est déployée. Il
appartient à l’application (ou à un script utilitaire) de rechercher les
fichiers de configuration à modifier et de les recharger.
L’utilisation du navigateur de fichiers dans kuard (accessible par
kubectl port-forward) est une excellente façon d’exploiter de manière
interactive les mises à jour dynamiques des secrets et des
ConfigMaps.

_ 11.5 RÉSUMÉ
Les ConfigMaps et les secrets sont un excellent moyen d’offrir une
configuration dynamique à votre application. Ils vous permettent de
créer une image de conteneur (et une définition de pod) une seule
fois et de la réutiliser dans des contextes différents. Vous pouvez
ainsi utiliser exactement la même image, que vous en soyez au
stade du développement, de la simulation ou bien de la production.
Vous pouvez aussi employer une seule image à travers plusieurs
équipes et services. Séparer la configuration du code de l’application
rendra vos applications plus fiables et plus réutilisables.
12

Déploiements

Jusqu’à présent, vous avez vu comment packager votre application


en tant que conteneur, créer un ReplicaSet de ces conteneurs et
utiliser des services pour équilibrer le trafic vers votre service. Tous
ces objets sont utilisés pour générer une seule instance de votre
application. Ils ne sont pas d’une grande utilité pour gérer la cadence
quotidienne ou hebdomadaire de la sortie des nouvelles versions de
votre application. En effet, les pods et les ReplicaSets sont censés
être liés à des images de conteneurs spécifiques qui ne changent
pas.
L’objet Deployment existe pour gérer la sortie de nouvelles versions.
Les déploiements représentent les applications déployées d’une
manière qui transcende toute version logicielle particulière de
l’application. En outre, les déploiements permettent de passer
facilement d’une version de votre code à la version suivante. Ce
processus de « déploiement » est configurable et minutieux.
L’utilisateur peut configurer une durée qui détermine le temps
d’attente entre la mise à niveau des pods individuels. Des contrôles
d’intégrité sont également utilisés pour s’assurer que la nouvelle
version de l’application fonctionne correctement, et le déploiement
est arrêté si trop de défaillances se produisent.
En utilisant des objets Deployment, vous pouvez simplement et de
manière fiable déployer de nouvelles versions de logiciels sans arrêt
de l’exploitation ni erreurs. La véritable mécanique de la mise en
œuvre du logiciel effectuée par un déploiement est contrôlée par un
contrôleur de déploiements qui s’exécute dans le cluster Kubernetes
lui-même. Cela signifie que vous pouvez laisser un déploiement
s’exécuter sans surveillance car il fonctionnera toujours
correctement et en toute sécurité. Il est donc facile d’intégrer les
déploiements avec de nombreux outils et services de livraison
continue. En outre, l’exécution côté serveur permet d’effectuer un
déploiement à partir d’endroits où la connexion Internet est
médiocre, voire intermittente. Imaginez le déploiement d’une
nouvelle version de votre logiciel à partir de votre smartphone tout
en étant dans le métro. Grâce à l’objet Deployment, ce scénario
devient possible et sécurisé !

Lorsque Kubernetes a été publié pour la première fois,


l’une des démonstrations les plus populaires de sa puissance a
été la mise à jour dynamique qui a prouvé qu’on pouvait utiliser
une seule commande pour mettre à jour sans problème une
application en cours d’exécution (aucun arrêt d’exploitation ni
perte de requêtes). Cette démonstration originale était basée
sur la commande kubectl rolling-update, qui est toujours
disponible dans l’outil de ligne de commande, mais dont la
fonctionnalité a été largement remplacée par l’objet
Deployment.

_ 12.1 VOTRE PREMIER DÉPLOIEMENT


Au début de ce livre, vous avez créé un pod en exécutant kubectl.
C’était une commande similaire à celle-ci :
$ kubectl run nginx --image=nginx:1.7.12

En interne, il s’agissait en fait de la création d’un objet Deployment.


Vous pouvez afficher cet objet Deployment en exécutant :
$ kubectl get deployments nginx
NAME DESIRED CURRENT UP-TO- AVAILABLE AGE
DATE

nginx 1 1 1 1 13s

12.1.1 Fonctionnement interne de l’objet Deployment

Examinons la manière dont les déploiements fonctionnent


réellement. Tout comme nous avons appris que les ReplicaSets
gèrent les pods, il faut savoir que les déploiements gèrent les
ReplicaSets. Comme pour toutes les relations dans Kubernetes,
cette relation est définie par des étiquettes et un sélecteur
d’étiquettes. Vous pouvez voir le sélecteur d’étiquettes en examinant
l’objet Deployment :
$ kubectl get deployments nginx \
-o jsonpath --template {.spec.selector.matchLabels}
map[run:nginx]

Vous pouvez ainsi constater que le déploiement gère un ReplicaSet


avec les étiquettes run=nginx. Nous pouvons utiliser cela dans une
requête de sélecteur d’étiquettes sur des ReplicaSets pour trouver
un ReplicaSet spécifique :
$ kubectl get replicasets --selector=run=nginx

NAME DESIRED CURRENT READY AGE

nginx-
1 1 1 13m
1128242161

Voyons maintenant la relation entre un déploiement et un ReplicaSet


opérationnel. Nous pouvons redimensionner le déploiement en
utilisant la commande scale :
$ kubectl scale deployments nginx --replicas=2
deployment "nginx" scaled

Maintenant, si nous listons à nouveau ce ReplicaSet, nous devrions


voir :
$ kubectl get replicasets --selector=run=nginx

NAME DESIRED CURRENT READY AGE

nginx-
2 2 2 13m
1128242161

La mise à l’échelle du déploiement a également fait évoluer le


ReplicaSet qu’il contrôle.
Maintenant, nous allons essayer l’inverse, c’est-à-dire la mise à
l’échelle du ReplicaSet :
$ kubectl scale replicasets nginx-1128242161 --replicas=1
replicaset "nginx-1128242161" scaled

Listez à nouveau le ReplicaSet :


$ kubectl get replicasets --selector=run=nginx

NAME DESIRED CURRENT READY AGE

nginx-
2 2 2 13m
1128242161

Il y a quelque chose de bizarre. En dépit de notre mise à l’échelle du


ReplicaSet sur un réplica, il a toujours deux réplicas dans l’état
désiré. Que se passe-t-il ? Rappelez-vous que Kubernetes est un
système d’auto-guérison en ligne. L’objet de niveau supérieur
Deployment gère ce ReplicaSet. Lorsque vous réglez le nombre de
réplicas à un, il ne correspond plus à l’état désiré du déploiement,
dont le nombre de réplicas est défini à 2. Le contrôleur de
déploiements remarque ceci et prend des mesures pour s’assurer
que l’état observé correspond à l’état désiré ; dans ce cas, il modifie
le nombre de réplicas à 2.
Si jamais vous voulez gérer directement ce ReplicaSet, vous devez
supprimer le déploiement (n’oubliez pas de définir --cascade à false,
sinon le ReplicaSet et les pods seront également supprimés).
_ 12.2 CRÉATION DE DÉPLOIEMENTS
Bien sûr, comme cela a été dit ailleurs, il est préférable de gérer de
manière déclarative vos configurations Kubernetes. Cela signifie que
vous devez conserver l’état de vos déploiements dans des fichiers
YAML ou JSON sur disque.
Pour commencer, téléchargez ce déploiement dans un fichier
YAML :
$ kubectl get deployments nginx --export -o yaml > nginx-deployment.yaml
$ kubectl replace -f nginx-deployment.yaml --save-config

Si vous regardez dans le fichier, vous verrez quelque chose


ressemblant à ceci :
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "1"
labels:
run: nginx
name: nginx
namespace: default
spec:
replicas: 2
selector:
matchLabels:
run: nginx
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
type: RollingUpdate
template:
metadata:
labels:
run: nginx
spec:
containers:
- image: nginx:1.7.12
imagePullPolicy: Always
dnsPolicy: ClusterFirst
restartPolicy: Always

Par souci de concision, un grand nombre de champs en


lecture seule et de valeurs par défaut ont été supprimés du
listing précédent. Nous avons également besoin d’exécuter
kubectl replace --saveconfig. Cela ajoute une annotation afin
que lors de l’application des futures modifications, kubectl
connaisse la dernière configuration appliquée pour une fusion
plus intelligente des configurations. Si vous utilisez toujours
kubectl apply, cette étape n’est requise qu’à l’issue de la
première fois où vous créez un déploiement à l’aide de kubectl
create -f.

La spécification d’un déploiement a une structure très similaire à


celle d’un ReplicaSet. Il existe un modèle de pod, qui contient un
certain nombre de conteneurs créés pour chaque réplica géré par le
déploiement. En plus de la spécification du pod, il y a aussi un objet
strategy :
...
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
type: RollingUpdate
...

L’objet strategy dicte les différentes façons dont le déploiement d’un


nouveau logiciel peut se passer. Deux stratégies différentes sont
prises en charge par les déploiements : Recreate et RollingUpdate.
Celles-ci sont étudiées en détail plus loin dans ce chapitre.

_ 12.3 GESTION DES DÉPLOIEMENTS


Comme pour tous les objets Kubernetes, vous pouvez obtenir des
informations détaillées sur votre déploiement via la commande
kubectl describe :
$ kubectl describe deployments nginx

Name: nginx

Namespace: default

CreationTimestamp: Sat, 31 Dec 2016 09:53:32 -0800

Labels: run=nginx

Selector: run=nginx

Replicas: 2 updated | 2 total | 2 available | 0 unavailable

StrategyType: RollingUpdate

MinReadySeconds: 0

RollingUpdateStrategy: 1 max unavailable, 1 max surge

OldReplicaSets: <none>

NewReplicaSet: nginx-1128242161 (2/2 replicas created)

Events:

FirstSeen ... Message


--------- ... -------

5m ... Scaled up replica set nginx-1128242161 to 1

4m ... Scaled up replica set nginx-1128242161 to 2

Dans la sortie, il y a beaucoup d’informations importantes.


Deux des informations les plus importantes de la sortie sont
OldReplicaSets et NewReplicaSet. Ces champs pointent vers les
objets ReplicaSet que ce déploiement gère actuellement. Si un
déploiement est en cours, les deux champs auront une valeur. Si un
déploiement est terminé, OldReplicaSets sera défini à <none>.
En plus de la commande describe, il y a aussi la commande kubectl
rollout pour les déploiements. Nous approfondirons l’étude de cette
commande plus tard, mais pour l’instant, vous pouvez utiliser kubectl
rollout history pour obtenir l’historique des déploiements associés à
un objet Deployment particulier. Si vous avez un déploiement en
cours, vous pouvez utiliser kubectl rollout status pour obtenir l’état
actuel d’un déploiement.

_ 12.4 MISE À JOUR DES DÉPLOIEMENTS


Les déploiements sont des objets déclaratifs qui décrivent une
application déployée. Les deux opérations les plus courantes
concernant un déploiement sont la mise à l’échelle et la mise à jour
des applications.

12.4.1 Mise à l’échelle d’un déploiement

Bien que nous ayons montré précédemment comment mettre à


l’échelle de manière impérative un déploiement à l’aide de la
commande kubectl scale, il est conseillé de gérer vos déploiements
de façon déclarative via les fichiers YAML, puis d’utiliser ces fichiers
pour mettre à jour votre déploiement. Pour faire évoluer un
déploiement, vous devez modifier votre fichier YAML pour
augmenter le nombre de réplicas :
...
spec:
replicas: 3
...

Une fois que vous avez enregistré et validé cette modification, vous
pouvez mettre à jour le déploiement à l’aide de la commande kubectl
apply :
$ kubectl apply -f nginx-deployment.yaml

Cela mettra à jour l’état souhaité du déploiement, en lui faisant


augmenter la taille du ReplicaSet qu’il gère, et finalement créer un
nouveau pod géré par le déploiement :
$ kubectl get deployments nginx

NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE

nginx 3 3 3 3 4m

12.4.2 Mise à jour d’une image de conteneur

L’autre cas d’utilisation courante pour la mise à jour d’un


déploiement consiste à déployer une nouvelle version du logiciel
exécuté dans un ou plusieurs conteneurs. Pour ce faire, vous devez
également modifier le fichier de déploiement YAML, mais dans ce
cas vous mettez à jour l’image du conteneur, plutôt que le nombre
de réplicas :
...
containers:
- image: nginx:1.9.10
imagePullPolicy: Always
...
Nous allons également mettre une annotation dans le modèle du
déploiement pour enregistrer des informations sur la mise à jour :
...
spec:
...
template:
metadata
annotations:
kubernetes.io/change-cause: "Update nginx to 1.9.10"
...

Assurez-vous d’ajouter cette annotation au modèle et non


au déploiement lui-même, puisque kubectl apply… utilise ce
champ dans l’objet Deployment. En outre, ne mettez pas à jour
l’annotation change-cause lors de l’exécution d’opérations
simples de mise à l’échelle. Une modification de change-cause
est une modification importante du modèle qui déclenchera un
nouveau déploiement.

Encore une fois, vous pouvez utiliser kubectl apply pour mettre à
jour le déploiement :
$ kubectl apply -f nginx-deployment.yaml

Après avoir mis à jour l’objet Deployment, cela déclenchera un


déploiement, que vous pourrez ensuite surveiller via la commande
kubectl rollout :
$ kubectl rollout status deployments nginx
deployment nginx successfully rolled out

Vous pouvez voir l’ancien et le nouveau ReplicaSets gérés par le


déploiement ainsi que les images utilisées. L’ancien et le nouveau
ReplicaSets sont conservés au cas où vous voudriez annuler
l’opération :
$ kubectl get replicasets -o wide

NAME DESIRED CURRENT READY ... IMAGE(S) ...

nginx-
0 0 0 ... ...
1128242161

nginx-
3 3 3 ... ...
1128635377

Si vous êtes au milieu d’un déploiement et que vous voulez le


suspendre temporairement pour une raison quelconque (par
exemple, si vous commencez à voir un comportement bizarre dans
votre système et que vous voulez étudier ce phénomène), vous
pouvez utiliser la commande pause :
$ kubectl rollout pause deployments nginx
deployment "nginx" paused

Si, après enquête, vous pensez que le déploiement peut se


poursuivre en toute sécurité, vous pouvez utiliser la commande
resume pour redémarrer là où vous vous étiez arrêté :
$ kubectl rollout resume deployments nginx
deployment "nginx" resumed

12.4.3 Historique des déploiements

Les déploiements Kubernetes maintiennent un historique des


déploiements, ce qui peut être utile à la fois pour comprendre l’état
précédent du déploiement et pour revenir à une version spécifique.
Vous pouvez voir l’historique des déploiements en exécutant :
$ kubectl rollout history deployment nginx
deployments "nginx"

REVISION CHANGE-CAUSE

1 <none>

2 Update nginx to 1.9.10


L’historique des révisions est donné de la plus ancienne à la plus
récente. Un numéro de révision unique est incrémenté pour chaque
nouveau déploiement. Jusqu’à présent, nous en avons deux : le
déploiement initial, et la mise à jour de l’image vers la version nginx
1.9.10.
Si vous êtes intéressé par des informations détaillées sur une
révision particulière, vous pouvez ajouter le paramètre --revision
pour afficher les détails d’une révision spécifique :
$ kubectl rollout history deployment nginx --revision=2
deployments "nginx" with revision #2
Labels: pod-template-hash=2738859366
run=nginx
Annotations: kubernetes.io/change-cause=Update nginx to 1.9.10
Containers:
nginx:
Image: nginx:1.9.10
Port:
Volume Mounts: <none>
Environment Variables: <none>
No volumes.

Faisons une mise à jour supplémentaire pour cet exemple. Mettez à


jour nginx vers la version 1.10.2 en modifiant le numéro de version
du conteneur et en mettant à jour l’annotation change-cause.
Exécutez la commande kubectl apply. Notre historique devrait
maintenant avoir trois entrées :
$ kubectl rollout history deployment nginx
deployments "nginx"

REVISION CHANGE-CAUSE

1 <none>

2 Update nginx to 1.9.10

3 Update nginx to 1.10.2


S’il y a un problème avec la dernière version et que vous voulez
revenir en arrière le temps que vous fassiez des recherches, vous
pouvez simplement annuler le dernier déploiement :
$ kubectl rollout undo deployments nginx
deployment "nginx" rolled back

La commande undo fonctionne indépendamment de l’étape du


déploiement. Vous pouvez annuler les déploiements partiellement
terminés ou complètement terminés. Une annulation d’un
déploiement est en fait simplement un déploiement en sens inverse
(par exemple, de la v2 à la v1, au lieu de la v1 à la v2), et toutes les
stratégies identiques qui contrôlent la stratégie de déploiement
s’appliquent également à la stratégie d’annulation. Vous pouvez
constater que l’objet Deployment ajuste simplement le nombre de
réplicas souhaités dans les ReplicaSets gérés :
$ kubectl get replicasets -o wide

NAME DESIRED CURRENT READY ... IMAGE(S) ...

nginx-1128242161 0 0 0 ... nginx:1.7.12 ...

nginx-1570155864 0 0 0 ... nginx:1.10.2 ...

nginx-2738859366 3 3 3 ... nginx:1.9.10 ...


Lorsque vous utilisez des fichiers déclaratifs pour
contrôler vos systèmes de production, vous voulez, dans la
mesure du possible, vous assurer que les manifestes contrôlés
correspondent à ce qui s’exécute réellement dans votre cluster.
Lorsque vous faites un kubectl rollout undo vous mettez à jour
l’état de production d’une manière qui n’est pas prise en compte
dans votre contrôle de code source.
Pour annuler un déploiement, il est sans doute préférable de
rétablir votre fichier YAML et d’exécuter une commande kubectl
apply pour revenir à la version précédente. De cette façon, votre
système de suivi de configuration est plus conforme à ce qui se
passe vraiment dans votre cluster.

Examinons à nouveau notre historique des déploiements :


$ kubectl rollout history deployment nginx
REVISION CHANGE-CAUSE

REVISION CHANGE-CAUSE

1 <none>

3 Update nginx to 1.10.2

4 Update nginx to 1.9.10

Il manque la révision 2 ! Il s’avère que lorsque vous revenez à une


révision précédente, le déploiement réutilise simplement le modèle
et le renumérote de telle sorte qu’il s’agit de la dernière révision. Ce
qui était auparavant la révision 2 a été maintenant réordonné en
révision 4.
Nous avons déjà vu que vous pouvez utiliser la commande kubectl
rollout undo pour revenir à une version précédente d’un
déploiement. En outre, vous pouvez revenir à une révision
spécifique dans l’historique en utilisant le paramètre --to-revision :
$ kubectl rollout undo deployments nginx --to-revision=3
deployment "nginx" rolled back
$ kubectl rollout history deployment nginx
deployments "nginx"

REVISION CHANGE-CAUSE

1 <none>

4 Update nginx to 1.9.10

5 Update nginx to 1.10.2

Encore une fois, la commande undo a pris la révision 3, l’a


appliquée, et l’a renumérotée en révision 5.
Si vous spécifiez une révision avec un numéro 0, c’est une façon
abrégée d’indiquer la révision précédente. De cette façon, kubectl
rollout undo équivaut à kubectl rollout undo --torevision=0.
Par défaut, l’historique complet de la révision d’un déploiement est
conservé en étant rattaché à l’objet Deployment lui-même. Au fil du
temps (c’est-à-dire des années) cet historique peut atteindre une
taille assez importante ; il est donc recommandé, si vous avez des
déploiements que vous prévoyez de conserver sur une longue
période, de définir la taille maximale de l’historique des révisions de
déploiement, afin de limiter la taille totale de l’objet Deployment. Par
exemple, si vous faites une mise à jour quotidienne, vous pouvez
limiter votre historique des révisions à 14, afin de conserver un
maximum de 2 semaines de révisions (si vous ne vous pensez pas
avoir besoin de revenir en arrière au-delà de 2 semaines).
Pour ce faire, utilisez la propriété revisionHistoryLimit dans la
spécification du déploiement :
...
spec:
# On fait des mises à jour quotidiennes ; on limite l’historique des révisions
# à deux semaines car on ne compte pas remonter au-delà.
revisionHistoryLimit: 14
...
_ 12.5 STRATÉGIES DE DÉPLOIEMENT
Quand il faut changer la version du logiciel implémentant votre
service, un déploiement Kubernetes prend en charge deux
stratégies de déploiement différentes :
Recreate
RollingUpdate

12.5.1 Stratégie Recreate

La stratégie Recreate est la plus simple des deux stratégies de


déploiement. Elle met à jour simplement le ReplicaSet qu’elle gère
pour utiliser la nouvelle image et met fin à tous les pods associés au
déploiement. Le ReplicaSet détecte qu’il n’a plus de réplicas et
recrée tous les pods à l’aide de la nouvelle image. Une fois les pods
recréés, ils exécutent la nouvelle version.
Bien que cette stratégie soit rapide et simple, elle a un inconvénient
majeur car elle est potentiellement catastrophique, et va fort
probablement aboutir à l’indisponibilité du site. C’est pour cette
raison que la stratégie Recreate ne doit être utilisée que pour les
déploiements de test où un service n’a pas d’impact sur l’utilisateur
et où une légère indisponibilité est acceptable.

12.5.2 Stratégie RollingUpdate

La stratégie RollingUpdate est en général la stratégie que l’on


préfère employer pour tout service qui a un impact sur l’utilisateur.
Bien qu’elle soit plus lente que la stratégie Recreate, elle est
également beaucoup plus sophistiquée et robuste. Avec la stratégie
RollingUpdate, vous pouvez déployer une nouvelle version de votre
service pendant qu’il reçoit le trafic des utilisateurs, sans aucun
temps d’arrêt du service.
Comme son nom le laisse supposer, la stratégie RollingUpdate
fonctionne en mettant à jour plusieurs pods à la fois, et en évoluant
progressivement jusqu’à ce que tous les pods exécutent la nouvelle
version de votre logiciel.

◆ Gestion des versions multiples de votre service

Cela signifie surtout que pendant une certaine période, à la fois la


nouvelle et l’ancienne version de votre service recevront des
demandes et devront gérer le trafic. Cela a des implications
importantes sur la façon dont vous devez concevoir votre logiciel. Il
est notamment capital que chaque version de votre logiciel, et tous
ses clients, soient capables de parler de manière interchangeable
avec une version légèrement plus ancienne et une version un peu
plus récente de votre logiciel.
Afin de vous montrer combien c’est important, étudiez le scénario
suivant :

Vous êtes en train de déployer votre logiciel frontend ; la moitié


de vos serveurs exécutent la version 1 et l’autre moitié
exécutent la version 2. Un utilisateur fait une première requête à
votre service et télécharge une bibliothèque JavaScript côté
client qui implémente votre interface utilisateur. Cette requête
est servie par un serveur de la version 1 et l’utilisateur reçoit
donc la bibliothèque cliente de la version 1. Cette bibliothèque
cliente s’exécute dans le navigateur de l’utilisateur et fait
ultérieurement d’autres requêtes à l’API de votre service. Ces
requêtes se
trouvent être routées vers un serveur de la version 2 si bien que
la version 1 de votre bibliothèque cliente JavaScript parle à la
version 2 de votre serveur API. Si vous ne vous êtes pas assuré
de la compatibilité entre ces versions, votre application ne
fonctionnera pas correctement.

Au début, cela peut apparaître comme un fardeau supplémentaire.


Mais en vérité, vous avez toujours eu ce problème, même si vous ne
l’avez peut-être pas remarqué. Concrètement, un utilisateur peut
faire une requête à l’instant t juste avant de lancer une mise à jour.
Cette requête est servie par un serveur de la version 1. À t_1 vous
mettez à jour votre service vers la version 2. À t_2 le code client de
la version 1 qui s’exécute sur le navigateur de l’utilisateur s’exécute
et atteint un point de terminaison d’API exploité par un serveur de la
version 2. Peu importe la façon dont vous mettez à jour votre
logiciel, vous devez maintenir la compatibilité ascendante et
descendante pour garantir des mises à jour fiables. La nature de la
stratégie RollingUpdate est simplement plus claire et explicite si bien
qu’elle doit être préférée.
Vous noterez que cela ne s’applique pas uniquement aux clients
JavaScript, la même chose étant vraie pour les bibliothèques
clientes qui sont compilées dans d’autres services qui font des
appels à votre service. Ce n’est pas parce que vous avez mis à jour
que cela signifie que les autres services ont mis à jour leurs
bibliothèques clientes. Ce type de compatibilité ascendante est
essentiel pour découpler votre service des systèmes qui dépendent
de votre service. Si vous ne formalisez pas vos API et ne découplez
pas vous-même, vous serez obligé de gérer minutieusement vos
déploiements avec tous les autres systèmes qui appellent votre
service. Avec ce type de couplage fort, il est extrêmement difficile de
maintenir le niveau d’agilité nécessaire pour pouvoir diffuser de
nouveaux logiciels chaque semaine, et encore plus toutes les heures
ou quotidiennement. Dans l’architecture découplée illustrée à la
figure 12.1, le frontend est isolé du backend via un contrat d’API et
un équilibreur de charge, alors que dans l’architecture couplée, un
client lourd compilé dans le frontend est utilisé pour se connecter
directement aux backends.

Figure 12.1 – Schémas des architectures d’application découplée


(à gauche) et couplée (à droite).
◆ Configuration d’une stratégie RollingUpdate

La stratégie RollingUpdate est une stratégie assez générique ; elle


peut être utilisée pour mettre à jour une grande variété d’applications
selon toute une série de paramètres différents. Par conséquent, la
mise à jour est elle-même parfaitement configurable et vous pouvez
définir son comportement en fonction de vos besoins particuliers. Il
existe deux paramètres que vous pouvez utiliser pour les réglages
du comportement de la mise à jour : maxUnavailable et maxSurge.
Le paramètre maxUnavailable définit le nombre maximal de pods qui
peuvent être indisponibles pendant une mise à jour. Ce paramètre
peut être défini sous la forme d’un nombre absolu (par exemple 3, ce
qui signifie qu’un maximum de trois pods peuvent être indisponibles)
ou bien sous la forme d’un pourcentage (par exemple 20, ce qui
signifie qu’un maximum de 20% du nombre désiré de réplicas
peuvent être indisponibles).
En règle générale, l’utilisation d’un pourcentage est une bonne
approche pour la plupart des services, puisque la valeur est
correctement applicable, quel que soit le nombre de réplicas
souhaité dans le déploiement. Il y a néanmoins des moments où
vous voudrez peut-être utiliser un nombre absolu (par exemple,
limiter le nombre maximal de pods indisponibles à un).
Fondamentalement, le paramètre maxUnavailable permet
d’optimiser la rapidité d’une mise à jour. Par exemple, si vous
définissez maxUnavailable à 50%, alors la mise à jour
redimensionnera immédiatement l’ancien ReplicaSet à 50% de sa
taille originale. Si vous avez quatre réplicas, la mise à l’échelle n’en
conservera plus que deux. La mise à jour remplacera alors les pods
supprimés en mettant à l’échelle le nouveau ReplicaSet pour qu’il ait
deux réplicas, ce qui fera un total de quatre réplicas (deux anciens et
deux nouveaux). Il y aura ensuite une mise à l’échelle de l’ancien
ReplicaSet pour qu’il n’y ait plus aucun réplica, ce qui aboutira à une
taille totale de deux nouveaux réplicas. Pour finir, il y aura une mise
à l’échelle du nouveau ReplicaSet qui contiendra quatre réplicas, ce
qui mettra un terme au déploiement. Ainsi, avec maxUnavailable
paramétré à 50%, notre déploiement se termine en quatre étapes,
mais avec seulement 50% de notre capacité de service à certains
moments.
Examinons ce qui se passe si l’on paramètre maxUnavailable à
25%. Dans cette situation, chaque étape n’est exécutée qu’avec un
seul réplica à la fois et il faut donc deux fois plus d’étapes pour que
le déploiement se termine, mais la disponibilité ne chute qu’à un
minimum de 75% pendant le déploiement. Cela illustre la manière
dont maxUnavailable nous permet de privilégier la vitesse de
déploiement par rapport à la disponibilité.

Les plus observateurs auront noté que la stratégie


Recreate est en fait identique à la stratégie RollingUpdate avec
maxUnavailable réglé à 100 %.

L’utilisation d’une capacité réduite pour réaliser un déploiement


réussi est utile lorsque votre service a des modèles de trafic cyclique
(par exemple, beaucoup moins de trafic la nuit) ou bien lorsque vous
avez des ressources limitées, si bien qu’une mise à l’échelle vers un
nombre de réplicas supérieur au nombre maximal n’est pas possible.
Il y a cependant des situations où vous ne voulez pas tomber en
dessous de la capacité de 100%, mais où vous êtes disposé à
utiliser temporairement des ressources supplémentaires afin
d’effectuer un déploiement. Dans ces cas-là, vous pouvez définir le
paramètre maxUnavailable à 0, et contrôler à la place le déploiement
à l’aide du paramètre maxSurge. Comme le paramètre
maxUnavailable, maxSurge peut être spécifié soit sous la forme d’un
nombre spécifique, soit sous la forme d’un pourcentage.
Le paramètre maxSurge contrôle le nombre de ressources
supplémentaires pouvant être créées pour réaliser un déploiement.
Pour illustrer comment cela fonctionne, imaginez que nous ayons un
service avec 10 réplicas. Nous avons défini maxUnavailable à 0 et
maxSurge à 20. La première chose que le déploiement va faire c’est
une mise à l’échelle du nouveau ReplicaSet en ajoutant 2 réplicas,
pour arriver à un total de 12 réplicas (120%) dans le service. Il y
aura ensuite un redimensionnement de l’ancien ReplicaSet à 8
réplicas, pour un total de 10 réplicas (8 anciens et 2 nouveaux) dans
le service. Ce processus se poursuit jusqu’à ce que le déploiement
soit terminé. À tout moment, la capacité du service est garantie à au
moins 100% et les ressources supplémentaires maximales utilisées
pour le déploiement sont limitées à 20% de toutes les ressources.

Le réglage de maxSurge à 100 % équivaut à un


déploiement bleu/vert. Le contrôleur de déploiements
redimensionne d’abord la nouvelle version pour qu’elle atteigne
100 % de l’ancienne version. Quand la nouvelle version est
stabilisée, l’ancienne version est immédiatement
redimensionnée pour atteindre 0 %.

12.5.3 Ralentissement des déploiements pour assurer


l’intégrité des services

Le but d’un déploiement progressif est de s’assurer que le


déploiement se traduit par un service sain et stable qui exécute la
nouvelle version du logiciel. Pour ce faire, le contrôleur de
déploiements attend toujours qu’un pod signale qu’il est prêt avant
de passer à la mise à jour du pod suivant.
Le contrôleur de déploiements examine le statut du pod
tel qu’il est déterminé par ses contrôles de disponibilité. Ces
contrôles, qui font partie des sondes d’intégrité du pod, sont
décrits en détail au chapitre 5. Si vous voulez utiliser des objets
Deployment pour déployer de manière fiable votre logiciel, vous
devez spécifier des vérifications d’intégrité et de disponibilité
pour les conteneurs de votre pod. Sans ces vérifications, le
contrôleur de déploiements travaille à l’aveugle.

Parfois, il arrive cependant que le fait que l’on vous signale qu’un
pod est prêt ne vous donne pas l’assurance que ce pod se comporte
effectivement correctement. En effet, certaines erreurs ne se
produisent qu’après un certain laps de temps. Par exemple, une fuite
de mémoire peut prendre quelques minutes avant qu’elle
n’apparaisse, ou bien vous pouvez avoir un bug qui ne soit
déclenché que par 1% des requêtes. Dans la plupart des situations
réelles, il est souhaitable d’attendre un certain temps pour s’assurer
que la nouvelle version fonctionne correctement avant de passer à la
mise à jour du prochain pod.
Pour les déploiements, cette période d’attente est définie par le
paramètre minReadySeconds :
...
spec:
minReadySeconds: 60
...

En réglant minReadySeconds à 60, on indique que le déploiement


doit attendre 60 secondes après avoir constaté l’intégrité d’un pod
avant de passer à la mise à jour du prochain pod.
En plus de ce délai, on peut également définir un délai d’attente qui
limite la durée pendant laquelle le système attendra. Supposons, par
exemple, que la nouvelle version de votre service comporte un bug
qui déclenche un blocage immédiat. Le service ne sera jamais prêt,
et en l’absence d’un délai d’attente, le contrôleur de déploiements va
paralyser à jamais votre déploiement.
Dans une telle situation, le bon comportement consiste à mettre un
terme au déploiement après un certain temps. À son tour, le
déploiement est signalé comme étant défaillant. Cet avertissement
de panne peut être utilisé pour déclencher une alerte qui peut
indiquer à un opérateur qu’il y a un problème avec le déploiement.

À première vue, la temporisation d’un déploiement peut


sembler être une complication inutile. Cependant, il arrive de
plus en plus souvent que des choses comme les déploiements
soient déclenchées par des systèmes entièrement automatisés
sans aucune intervention humaine. Dans une telle situation, la
temporisation devient une exception critique, qui peut
déclencher une restauration automatique de la version ou bien
créer un événement qui déclenche une intervention humaine.

Pour définir ce délai d’attente, on utilise le paramètre de déploiement


progress DeadlineSeconds :
...
spec:
progressDeadlineSeconds: 600
....

Cet exemple montre comment définir un délai d’exécution à


10 minutes. Si une étape particulière du déploiement ne parvient pas
à se réaliser en 10 minutes, le déploiement est marqué comme
ayant échoué, et toutes les tentatives de progression du déploiement
sont interrompues.
Il est important de noter que ce délai exprime une progression du
déploiement, et non pas la longueur totale d’un déploiement. Dans
ce contexte, la progression est définie comme le moment où le
déploiement crée ou supprime un pod. Lorsque cela se produit,
l’horloge de la temporisation est remise à zéro. La figure 12.2 illustre
le cycle de vie d’un déploiement.

Figure 12.2 – Cycle de vie d’un déploiement Kubernetes.

_ 12.6 SUPPRESSION D’UN


DÉPLOIEMENT
Si vous voulez supprimer un déploiement, vous pouvez le faire soit
avec la commande suivante :
$ kubectl delete deployments nginx

soit en utilisant le fichier YAML que nous avons créé


précédemment :
$ kubectl delete -f nginx-deployment.yaml

Dans les deux cas, par défaut, la suppression d’un déploiement


supprime l’intégralité du service. Cela supprimera non seulement le
déploiement, mais aussi les ReplicaSets gérés par le déploiement,
ainsi que les pods gérés par les ReplicaSets. Comme pour les
ReplicaSets, si ce n’est pas ce que vous souhaitez, vous pouvez
utiliser le paramètre --cascade=false pour supprimer uniquement
l’objet Deployment.
_ 12.7 RÉSUMÉ
L’objectif principal de Kubernetes est de faciliter la création et le
déploiement de systèmes distribués fiables. Cela signifie non
seulement l’instanciation de l’application la première fois, mais aussi
la gestion du déploiement régulier de nouvelles versions de ce
service. L’objet Deployment est un élément essentiel de la fiabilité et
de la gestion du déploiement de vos services.
13

Intégration des solutions


de stockage à Kubernetes

Dans de nombreux cas, le fait de découpler l’état et les applications


tout en créant des micro-services aussi stateless que possible
produit des systèmes hautement fiables et gérables.
Cependant la plupart des systèmes ont une certaine complexité et
sont quelque part stateful, qu’il s’agisse des enregistrements d’une
base de données ou bien des index qui permettent d’accéder aux
résultats d’un moteur de recherche Web. À un certain moment, il faut
bien que vous ayez des données stockées quelque part.
L’intégration de ces données à des conteneurs et à des solutions
d’orchestration de conteneurs est souvent l’aspect le plus compliqué
de la construction d’un système distribué. Cette complexité découle
en grande partie du fait que la migration vers les architectures en
conteneurs est aussi une migration vers le développement
d’applications découplées, immutables et déclaratives. Ces modèles
sont relativement faciles à appliquer aux applications Web stateless,
mais même les solutions de stockage « cloud-native » comme
Cassandra ou MongoDB supposent une sorte de procédure
manuelle ou impérative pour mettre en place une solution fiable et
répliquée.
À titre d’exemple, envisagez la configuration d’un ReplicaSet dans
MongoDB : cela implique le déploiement du démon Mongo, puis
l’exécution d’une commande impérative pour identifier le leader,
ainsi que les participants dans le cluster Mongo. Bien entendu, ces
étapes peuvent faire l’objet d’un script, mais dans un monde de
conteneurs, il est difficile de voir comment intégrer ces commandes
dans un déploiement. De la même manière, la simple obtention de
noms pouvant être résolus par le DNS pour des conteneurs
individuels dans un ensemble répliqué de conteneurs est délicate.
La complexité supplémentaire vient du fait qu’il y a la gravitation des
données6. La plupart des systèmes en conteneurs ne sont pas
construits à partir de rien ; ils sont généralement adaptés à partir de
systèmes existants déployés sur des machines virtuelles, et ces
systèmes incluent souvent des données qui doivent être importées
ou migrées.
Enfin, l’évolution vers le cloud signifie souvent que le stockage est
en fait un service de cloud externalisé, et dans ce contexte, il ne
peut jamais vraiment exister à l’intérieur du cluster Kubernetes.
Ce chapitre étudie de nombreuses approches pour intégrer le
stockage dans les micro-services en conteneurs de Kubernetes.
Tout d’abord, nous examinons comment importer des solutions de
stockage externes existantes (sur des services dans le cloud ou sur
des machines virtuelles) dans Kubernetes. Ensuite, nous explorons
la façon d’exécuter des singletons fiables à l’intérieur de Kubernetes
qui vous permettent de bénéficier d’un environnement qui
correspond en grande partie aux machines virtuelles où vous avez
déployé précédemment des solutions de stockage. Enfin, nous
étudions les StatefulSets, qui sont encore en cours de
développement, mais qui représentent l’avenir des charges de travail
stateful dans Kubernetes.

_ 13.1 IMPORTATION DE SERVICES


EXTERNES
Dans de nombreux cas, vous avez une machine existante en cours
d’exécution sur votre réseau qui exploite une base de données.
Dans cette situation, vous ne souhaitez peut-être pas déplacer
immédiatement cette base de données dans des conteneurs gérés
par Kubernetes. Il peut y avoir de multiples raisons à cela : la base
de données peut être gérée par une autre équipe, vous faites une
migration graduelle, ou la tâche de migration des données comporte
tout simplement plus d’inconvénients que d’avantages.
Quelles que soient les raisons pour laisser les choses en l’état,
l’ancien serveur et le service associé ne vont pas migrer dans
Kubernetes, mais il est cependant toujours intéressant de
représenter ce serveur dans Kubernetes. Lorsque vous faites cela,
vous profitez de toutes les primitives intégrées de découverte des
services et de nommage fournies par Kubernetes. En outre, cela
vous permet de configurer toutes vos applications de telle sorte qu’il
semble que la base de données qui est en cours d’exécution sur une
machine de votre réseau est en fait un service Kubernetes. Cela
signifie qu’il est alors facile de la remplacer par une base de
données qui est un service Kubernetes. Par exemple, dans votre
environnement de production, vous pouvez exploiter votre ancienne
base de données qui s’exécute sur un ordinateur, mais pour les tests
en continu, vous pouvez déployer une base de données de test sous
la forme d’un conteneur transitoire. Comme il est créé et supprimé
pour chaque série de tests, la persistance des données n’est pas
importante dans le cas de tests en continu. Le fait de représenter les
deux bases de données sous la forme de services Kubernetes vous
permet de maintenir des configurations identiques dans les
environnements de test et de production. Cette grande similitude
entre les tests et la production garantit que des tests réussis seront
synonymes d’un déploiement réussi dans l’environnement de
production.
Pour voir concrètement comment on peut maintenir une conformité
élevée entre le développement et la production, vous devez vous
souvenir que tous les objets Kubernetes sont déployés dans des
espaces de noms. Imaginez que nous ayons défini les espaces de
noms test et production. Le service de test est importé à l’aide d’un
objet similaire à celui-ci :
kind: Service
metadata:
name: my-database
# notez ici l’espace de noms 'test'
namespace: test
...

Le service de production est identique, mais il utilise un espace de


noms différent :
kind: Service
metadata:
name: my-database
# notez ici l’espace de noms 'product'
namespace: product
...

Lorsque vous déployez un pod dans l’espace de noms test et qu’il


recherche le service nommé my-database, il recevra un pointeur
vers my-database.test.svc.cluster.internal, qui à son tour va pointer
vers la base de données de test. En revanche, lorsqu’un pod
déployé dans l’espace de noms product recherche le même nom
(my-database) il recevra un pointeur vers my-
database.prod.svc.cluster.internal, qui est la base de données de
production. Ainsi, le même nom de service, dans deux espaces de
noms différents, est résolu en deux services différents. Pour plus de
détails sur la façon dont cela fonctionne, consultez le chapitre 7.

Les techniques suivantes utilisent toutes une base de


données ou d’autres services de stockage, mais ces approches
peuvent également être utilisées avec d’autres services qui ne
fonctionnent pas à l’intérieur de votre cluster Kubernetes.

13.1.1 Services sans sélecteurs

Lorsque nous avons introduit les services, nous avons longuement


parlé des requêtes d’étiquettes et de la façon dont elles sont
utilisées pour identifier l’ensemble dynamique des pods qui étaient
les backends d’un service particulier. Avec les services externes,
cependant, il n’existe pas de requête d’étiquette de ce type. À la
place, vous avez généralement un nom DNS qui pointe vers le
serveur spécifique exécutant la base de données. Pour notre
exemple, supposons que ce serveur s’appelle
database.company.com. Pour importer ce service de base de
données externe dans Kubernetes, on commence par créer un
service sans sélecteur de pod qui référence le nom DNS du serveur
de base de données (exemple 13-1).
Exemple 13-1. dns-service.yaml
kind: Service
apiVersion: v1
metadata:
name: external-database
spec:
type: ExternalName
externalName: database.company.com

Lorsqu’un service Kubernetes typique est créé, une adresse IP est


également créée et le service DNS Kubernetes est rempli avec un
enregistrement A qui pointe sur cette adresse IP. Lorsque vous
créez un service de type ExternalName, le service DNS Kubernetes
est rempli à la place avec un enregistrement CNAME qui pointe sur
le nom externe que vous avez spécifié (database.company.com
dans notre exemple). Lorsqu’une application du cluster effectue une
recherche DNS sur le nom d’hôte external-
database.svc.default.cluster, le protocole DNS transforme ce nom en
« database.company.com ». Il y a ensuite une résolution vers
l’adresse IP de votre serveur de base de données externe. De cette
façon, tous les conteneurs de Kubernetes croient qu’ils parlent à un
service qui est hébergé avec d’autres conteneurs, alors qu’en fait, ils
sont redirigés vers la base de données externe.
Vous noterez que cela ne se limite pas aux bases de données que
vous exécutez sur votre propre infrastructure. De nombreuses bases
de données hébergées dans le cloud et d’autres services vous
fournissent un nom DNS à utiliser lors de l’accès à la base de
données (par exemple, my-database.databases.cloudprovider.com).
Vous pouvez utiliser ce nom DNS en tant que externalName. Cela
importe la base de données fournie par le service de cloud dans
l’espace de noms de votre cluster Kubernetes.
Il arrive cependant parfois que vous n’ayez pas d’adresse DNS pour
un service de base de données externe, mais uniquement une
adresse IP. En pareil cas, il est encore possible d’importer ce
serveur en tant que service Kubernetes, mais l’opération est un peu
différente. Tout d’abord, on crée un service sans sélecteur
d’étiquettes, mais aussi sans type ExternalName que nous avons
utilisé avant (exemple 13-2).
Exemple 13-2. external-ip-service.yaml
kind: Service
apiVersion: v1
metadata:
name: external-ip-database

À ce stade, Kubernetes va allouer une adresse IP virtuelle pour ce


service et remplir un enregistrement A. Toutefois, comme il n’y a pas
de sélecteur pour le service, il n’y aura pas de points de terminaison
remplis pour l’équilibreur de charge afin de rediriger le trafic.
Étant donné qu’il s’agit d’un service externe, l’utilisateur est
responsable du remplissage manuel des points de terminaison avec
une ressource Endpoints (Exemple 13-3).
Exemple 13-3. external-ip-endpoints.yaml
kind: Endpoints
apiVersion: v1
metadata:
name: external-ip-database
subsets:
- addresses:
- ip: 192.168.0.1
ports:
- port: 330
Si vous avez plus d’une adresse IP pour la redondance, vous
pouvez les répéter dans le tableau des adresses. Une fois les points
de terminaison remplis, l’équilibreur de charge va commencer à
rediriger le trafic de votre service Kubernetes vers les adresses IP
des points de terminaison.

Comme l’utilisateur assume la responsabilité de garder


l’adresse IP du serveur à jour, vous devez vous assurer qu’elle
ne change jamais ou bien garantir qu’un processus automatisé
met à jour l’enregistrement Endpoints.

13.1.2 Limitations des services externes : contrôle


d’intégrité

Les services externes dans Kubernetes ont une restriction


importante : ils n’effectuent aucun contrôle d’intégrité. C’est à
l’utilisateur de s’assurer que le point de terminaison ou le nom DNS
fourni à Kubernetes est suffisamment fiable pour garantir la sécurité
de l’application.

_ 13.2 EXÉCUTION DE SINGLETONS


FIABLES
Le problème de l’exécution de solutions de stockage dans
Kubernetes est que souvent les primitives comme les ReplicaSets
s’attendent à ce que chaque conteneur soit identique et
remplaçable, ce qui n’est cependant pas le cas pour la plupart des
solutions de stockage. Pour y remédier, on peut utiliser des
primitives Kubernetes, mais sans tenter de répliquer le stockage. Il
suffit alors d’exécuter un seul pod qui exploite la base de données
ou une autre solution de stockage. De cette façon, il n’y a plus de
problème d’exécution de stockage répliqué dans Kubernetes, dans
la mesure où il n’y a pas de réplication.
À première vue, cela peut sembler aller à l’encontre des principes de
la construction de systèmes distribués fiables, mais en général, cela
n’est pas moins fiable que d’exécuter votre base de données ou
votre infrastructure de stockage sur une seule machine, qu’elle soit
virtuelle ou physique, ce qui est la manière dont la plupart des gens
construisent actuellement leurs systèmes. En effet, si vous
structurez le système correctement, le seul problème réel que vous
risquez de rencontrer est un arrêt potentiel de l’exploitation en raison
d’une mise à jour ou bien à cause d’une panne de machine. Alors
que pour les systèmes de grande envergure ou les systèmes
critiques cette indisponibilité peut se révéler inacceptable, pour de
nombreuses applications de taille plus petite un temps d’arrêt limité
est un compromis raisonnable étant donné la réduction de la
complexité du système. Si ce n’est pas votre cas, n’hésitez pas à
sauter cette section ou bien à importer des services existants
comme cela est décrit dans la section précédente, ou à migrer vers
des StatefulSets natifs de Kubernetes, qui sont décrits dans la
section suivante. Pour tous les autres, nous allons étudier la manière
de créer des singletons fiables pour le stockage des données.

13.2.1 Exécution d’un singleton MySQL

Dans cette section, nous allons décrire comment exécuter une


instance de singleton fiable de la base de données MySQL sous la
forme d’un pod dans Kubernetes et voir comment exposer ce
singleton à d’autres applications du cluster.
Pour ce faire, nous allons créer trois objets de base :
Un volume persistant pour gérer la durée de vie du stockage sur
disque indépendamment de la durée de vie de l’application
MySQL en cours d’exécution ;
Un pod MySQL qui va exécuter l’application MySQL ;
Un service qui va exposer ce pod à d’autres conteneurs du
cluster.
Dans le chapitre 5, nous avons décrit les volumes persistants, mais
un rappel n’est pas inutile. Un volume persistant est un
emplacement de stockage qui a une durée de vie indépendante d’un
pod ou d’un conteneur. C’est très utile dans le cas de solutions de
stockage persistantes où la représentation sur disque d’une base de
données doit subsister même si les conteneurs qui exécutent
l’application de base de données plantent ou sont déplacés sur
d’autres ordinateurs. Si l’application est déplacée sur un autre
ordinateur, le volume doit se déplacer avec elle et les données
doivent être conservées. En séparant le stockage de données sous
la forme d’un volume persistant, on rend cela possible. Pour
commencer, nous allons créer un volume persistant pour notre base
de données MySQL.
Cet exemple utilise NFS pour une portabilité maximale, mais
Kubernetes prend en charge de nombreux types de volumes
persistants différents. Par exemple, il existe des pilotes de volumes
persistants pour tous les principaux fournisseurs de cloud public,
ainsi que de nombreux fournisseurs de cloud privé. Pour utiliser ces
solutions, il suffit de remplacer nfs par le type de volume du
fournisseur de cloud approprié (par exemple, azure,
awsElasticBlockStore ou gcePersistentDisk). Dans tous les cas,
vous n’avez besoin de changer que cela. Kubernetes sait comment
créer le disque de stockage approprié pour chaque fournisseur de
cloud. C’est un excellent exemple de la façon dont Kubernetes
simplifie le développement de systèmes distribués fiables.
Voici un exemple d’objet de volume persistant (exemple 13-4).
Exemple 13-4. nfs-volume.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: database
labels:
volume: my-volume
spec:
accessModes:
- ReadWriteMany
capacity:
storage: 1Gi
nfs:
server: 192.168.0.1
path: "/exports"

Ce fichier définit un objet de volume persistant NFS avec 1 Go


d’espace de stockage.
Nous pouvons créer ce volume persistant comme d’habitude avec la
commande :
$ kubectl apply -f nfs-volume.yaml

Maintenant que nous avons créé un volume persistant, nous devons


demander ce volume persistant pour notre pod, avec un objet
PersistentVolumeClaim (exemple 13-5).
Exemple 13-5. nfs-volume-claim.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: database
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
selector:
matchLabels:
volume: my-volume

Le champ selector utilise des étiquettes pour trouver le volume


correspondant que nous avons défini précédemment.
Ce genre d’indirection peut sembler trop compliqué, mais il a pour
objectif d’isoler notre définition de pod de notre définition du
stockage. Vous pouvez déclarer des volumes directement à
l’intérieur d’une spécification de pod, mais cela verrouille la
spécification du pod à un fournisseur de volume particulier (par
exemple, un fournisseur spécifique de cloud public ou privé). En
utilisant les demandes de volumes, vous pouvez conserver vos
spécifications de pod sans faire référence à un fournisseur de cloud
spécifique ; créez simplement des volumes différents, spécifiques au
cloud, et utilisez un PersistentVolumeClaim pour les lier.
Maintenant que nous avons demandé notre volume, nous pouvons
utiliser un ReplicaSet pour construire notre pod de singleton. Il peut
sembler étrange d’utiliser un ReplicaSet pour gérer un seul pod,
mais c’est nécessaire pour assurer la fiabilité. Rappelez-vous qu’une
fois qu’il est ordonnancé sur une machine, un pod est lié à jamais à
cette machine. Si celle-ci a une défaillance, alors tous les pods qui
sont sur cette machine et qui ne sont pas gérés par un contrôleur de
niveau supérieur comme un ReplicaSet, disparaissent avec la
machine et ne sont pas à nouveau ordonnancés ailleurs. Par
conséquent, pour nous assurer que notre pod de base de données
est replanifié en cas de panne d’une machine, nous utilisons le
contrôleur de ReplicaSets de niveau supérieur, avec une taille de
réplica fixée à un, afin de gérer notre base de données (exemple 13-
6).
Exemple 13-6. mysql-replicaset.yaml
apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
name: mysql
# étiquettes afin de lier un service à ce pod
labels:
app: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: database
image: mysql
resources:
requests:
cpu: 1
memory: 2Gi
env:
# Les variables d’environnement ne sont pas idéales pour la sécurité,
# mais on les utilise ici par souci de concision.
# Voir le chapitre 11 pour des solutions plus appropriées.
- name: MYSQL_ROOT_PASSWORD
value: some-password-here
livenessProbe:
tcpSocket:
port: 3306
ports:
- containerPort: 3306
volumeMounts:
- name: database
# /var/lib/mysql est l’endroit où MySQL stocke sa base de données
mountPath: "/var/lib/mysql"
volumes:
- name: database
persistentVolumeClaim:
claimName: database

Une fois que nous avons créé le ReplicaSet, il va à son tour créer un
pod exécutant MySQL, en utilisant le disque persistant que nous
avons créé au départ. La dernière étape consiste à exposer cela
sous la forme d’un service Kubernetes (exemple 13-7).
Exemple 13-7. mysql-service.yaml
apiVersion: v1
kind: Service

metadata:
name: mysql
spec:
ports:
- port: 3306
protocol: TCP
selector:
app: mysql

Maintenant, nous avons une instance fiable d’un singleton MySQL


en cours d’exécution dans notre cluster et qui est exposée sous la
forme d’un service nommé mysql ; nous pouvons accéder à ce
service avec le nom de domaine complet mysql.svc.default.cluster.
Des instructions similaires peuvent être utilisées pour une grande
variété d’entrepôts de données ; si vos besoins sont simples et que
vous pouvez survivre à un temps d’arrêt limité à cause d’une panne
de machine ou d’une mise à jour du logiciel de base de données, un
singleton fiable peut représenter la bonne approche pour le stockage
de votre application.

13.2.2 Provisionnement dynamique du volume

De nombreux clusters comprennent également un provisionnement


dynamique du volume. Avec le provisionnement dynamique de
volume, l’opérateur de cluster crée un ou plusieurs objets
StorageClass. Voici une classe de stockage par défaut qui
provisionne automatiquement des objets disque sur la plateforme
Microsoft Azure (exemple 13-8).
Exemple 13-8. storageclass.yaml
apiVersion: storage.k8s.io/v1beta1
kind: StorageClass
metadata:
name: default
annotations:
storageclass.beta.kubernetes.io/is-default-class: "true"
labels:
kubernetes.io/cluster-service: "true"
provisioner: kubernetes.io/azure-disk
Une fois qu’une classe de stockage a été créée pour un cluster, vous
pouvez faire référence à cette classe de stockage dans votre
demande de volume persistant, plutôt que de faire référence à un
volume persistant spécifique. Lorsque le provisionneur dynamique
voit cette demande de stockage, il utilise le pilote de volume
approprié pour créer le volume et le lier à votre demande de volume
persistant.
Voici un exemple de demande PersistentVolumeClaim qui utilise la
classe de stockage default que nous venons de définir pour
demander un volume persistant qui vient d’être créé (exemple 13-9).
Exemple 13-9. dynamic-volume-claim.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: my-claim
annotations:
volume.beta.kubernetes.io/storage-class: default
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi

L’annotation volume.beta.kubernetes.io/storage-class est ce qui lie


cette demande à la classe de stockage que nous avons créée.
Les volumes persistants sont parfaits pour les applications
traditionnelles qui nécessitent un stockage, mais si vous avez besoin
de développer un stockage évolutif ayant une haute disponibilité
dans un style propre à Kubernetes, vous pouvez employer le nouvel
objet StatefulSet. C’est dans cette optique que nous allons décrire
comment déployer MongoDB en utilisant des StatefulSets dans la
prochaine section.
_ 13.3 STOCKAGE NATIF KUBERNETES
AVEC DES STATEFULSETS
Lorsque Kubernetes a été développé, nous avons insisté lourdement
sur l’homogénéité de tous les réplicas dans un ReplicatSet. Dans
cette conception, aucun réplica n’avait d’identité ou de configuration
individuelle. C’était au développeur de l’application individuelle de
déterminer une conception qui pourrait établir cette identité de
l’application.
Bien que cette approche permette de bien isoler le système
d’orchestration, elle rend assez difficile le développement
d’applications stateful. Après une importante contribution de la
communauté et de nombreuses expérimentations avec différentes
applications stateful, les StatefulSets ont été introduits dans la
version 1.5 de Kubernetes.

Comme les StatefulSets sont une fonctionnalité bêta, il


est possible que l’API soit modifiée avant qu’elle n’intègre l’API
Kubernetes officielle. L’API StatefulSet a reçu beaucoup de
commentaires et elle est en général considérée comme étant
assez stable, mais son statut bêta doit être pris en considération
avant d’exploiter des StatefulSets. Dans de nombreux cas, les
modèles précédemment décrits pour les applications stateful
peuvent être plus appropriés à court terme.

13.3.1 Propriétés des StatefulSets

Les StatefulSets sont des groupes répliqués de pods qui sont


similaires à des ReplicaSets, mais contrairement à un ReplicaSet, ils
ont certaines propriétés uniques :
Chaque réplica obtient un nom d’hôte persistant avec un indice
unique (par exemple, database-0, database-1, etc.).
Chaque réplica est créé dans l’ordre croissant des indices, et la
création se bloque tant que le pod à l’indice précédent n’est pas
sain et disponible. Cela vaut également quand on augmente le
nombre de réplicas.
Une fois supprimé, chaque réplica sera supprimé dans l’ordre
décroissant. Cela s’applique également quand on réduit le
nombre de réplicas.

13.3.2 MongoDB répliqué manuellement avec


des StatefulSets

Dans cette section, nous allons déployer un cluster MongoDB


répliqué. Pour l’instant, la configuration de la réplication sera faite
manuellement pour vous donner une idée de la façon dont les
StatefulSets fonctionnent. À la fin, nous allons aussi automatiser
cette configuration.
Pour commencer, nous allons créer un ensemble répliqué de trois
pods MongoDB à l’aide d’un objet StatefulSet (exemple 13-10).
Exemple 13-10. mongo-simple.yaml
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: mongo
spec:
serviceName: "mongo"
replicas: 3
template:
metadata:
labels:
app: mongo
spec:
containers:
- name: mongodb
image: mongo:3.4.1
command:
- mongod
- --replSet
- rs0
ports:
- containerPort: 27017
name: peer

Comme vous pouvez le voir, la définition est similaire à la définition


du ReplicaSet des sections précédentes. Les seuls changements
sont les champs apiVersion et kind. Créez le StatefulSet avec la
commande :
$ kubectl apply -f mongo-simple.yaml

Une fois créé, les différences entre un ReplicaSet et un StatefulSet


deviennent évidentes. Exécutez kubectl get pods et vous verrez
quelque chose similaire à ceci :

NAME READY STATUS RESTARTS AGE

mongo-0 1/1 Running 0 1m

mongo-1 0/1 ContainerCreating 0 10s

Il y a deux différences importantes entre cet affichage et ce que vous


verriez avec un ReplicaSet. La première est que chaque pod
répliqué a un indice numérique (0, 1…), au lieu du suffixe aléatoire
qui est ajouté par le contrôleur de ReplicaSets. La seconde est que
les pods sont créés dans l’ordre, les uns à la suite des autres, et non
pas tous à la fois, comme c’est le cas avec un ReplicaSet.
Une fois que le StatefulSet est créé, nous avons également besoin
de créer un service « headless » pour gérer les entrées DNS du
StatefulSet. Dans Kubernetes, un service est appelé « headless »
s’il n’a pas d’adresse IP virtuelle de cluster. Puisqu’avec les
StatefulSets chaque pod a une identité unique, il n’y a pas vraiment
d’intérêt à avoir une adresse IP d’équilibrage de charge pour le
service répliqué. Vous pouvez créer un service headless en utilisant
clusterIP: None dans la spécification du service (exemple 13-11).
Exemple 13-11. mongo-service.yaml
apiVersion: v1
kind: Service
metadata:
name: mongo
spec:
ports:
- port: 27017
name: peer
clusterIP: None
selector:
app: mongo

Une fois que vous avez créé ce service, il y a généralement quatre


entrées DNS qui sont remplies. Comme d’habitude,
mongo.default.svc.cluster.local est créé, mais à la différence d’un
service standard, faire une recherche DNS sur ce nom d’hôte fournit
toutes les adresses dans le StatefulSet. En outre, les entrées sont
créées pour mongo-0.mongo.default.svc.cluster.local ainsi que pour
mongo-1.mongo et mongo-2.mongo. Chacune de ces entrées est
résolue avec l’adresse IP spécifique de l’indice du réplica dans le
StatefulSet. Ainsi, avec les StatefulSets, vous obtenez des noms
bien définis et persistants pour chaque réplica du ReplicaSet. C’est
souvent très utile lorsque vous configurez une solution de stockage
répliqué. Vous pouvez voir ces entrées DNS en exécutant des
commandes dans l’un des réplicas Mongo :
$ kubectl run -it --rm --image busybox busybox ping mongo-1.mongo

Ensuite, nous allons configurer manuellement la réplication Mongo


en utilisant ces noms d’hôte par pod.
Nous allons choisir mongo-0.mongo comme réplica principal.
Exécutez l’outil mongo dans ce pod :
$ kubectl exec -it mongo-0 mongo
> rs.initiate( {
_id: "rs0",
members:[ { _id: 0, host: "mongo-0.mongo:27017" } ]
});
OK

Cette commande indique à mongodb de lancer le ReplicaSet rs0


avec mongo-0. mongo comme réplica principal.

Le nom rs0 est arbitraire. Vous pouvez utiliser ce que


vous voulez, mais vous aurez besoin de le changer dans le
fichier mongo.yaml de la définition du StatefulSet.

Une fois que vous avez initié le ReplicaSet Mongo, vous pouvez
ajouter les réplicas restants en exécutant les commandes suivantes
dans l’outil mongo sur le pod mongo-0.mongo :
$ kubectl exec -it mongo-0 mongo
> rs.add("mongo-1.mongo:27017");
> rs.add("mongo-2.mongo:27017");

Comme vous pouvez le voir, nous utilisons des noms DNS


spécifiques aux réplicas pour les ajouter en tant que réplicas dans
notre cluster Mongo. À ce stade, nous avons terminé. Notre système
MongoDB répliqué est opérationnel, mais il n’est pas suffisamment
automatisé à notre goût. Dans la section suivante, nous allons voir
comment utiliser des scripts pour automatiser le programme de
configuration.

13.3.3 Automatisation de la création de clusters


MongoDB

Pour automatiser le déploiement de notre cluster MongoDB basé sur


des StatefulSets, nous allons ajouter un conteneur supplémentaire à
nos pods pour effectuer l’initialisation.
Pour configurer ce pod sans avoir à construire une nouvelle image
Docker, nous allons utiliser un ConfigMap pour ajouter un script à
l’image MongoDB existante. Voici le conteneur que nous ajoutons :
...
- name: init-mongo
image: mongo:3.4.1
command:
- bash
- /config/init.sh
volumeMounts:
- name: config
mountPath: /config
volumes:
- name: config
configMap:
name: "mongo-init"

Vous noterez que le conteneur monte un volume ConfigMap dont le


nom est mongo-init. Ce ConfigMap contient un script qui effectue
notre initialisation. Tout d’abord, le script détermine s’il s’exécute sur
mongo-0. Si c’est le cas, il crée le ReplicaSet en utilisant la même
commande que nous avons utilisée de manière impérative
précédemment. S’il est sur un autre réplica Mongo, il attend jusqu’à
ce que le ReplicaSet existe, puis il s’inscrit en tant que membre de
ce ReplicaSet.
L’exemple 13-2 contient l’objet ConfigMap complet.
Exemple 13-12. mongo-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mongo-init
data:
init.sh: |
#!/bin/bash

# Il faut attendre le contrôle d’intégrité et de disponibilité pour


# que les noms mongo soient résolus, ce qui n’est pas très satisfaisant.
until ping -c 1 ${HOSTNAME}.mongo; do
echo "attente du DNS (${HOSTNAME}.mongo)..."
sleep 2
done

until /usr/bin/mongo --eval 'printjson(db.serverStatus())'; do


echo "connexion au mongo local..."
sleep 2
done
echo "connecté au local."

HOST=mongo-0.mongo:27017

until /usr/bin/mongo --host=${HOST} --eval 'printjson(db.serverStatus())'; do


echo "connexion au mongo distant..."
sleep 2
done
echo "connecté au distant."

if [[ "${HOSTNAME}" != 'mongo-0' ]]; then


until /usr/bin/mongo --host=${HOST} --eval="printjson(rs.status())" \
| grep -v "no replset config has been received"; do
echo "attente de l’initialisation de la réplication"
sleep 2
done
echo "je m’ajoute à mongo-0"
/usr/bin/mongo --host=${HOST} \
--eval="printjson(rs.add('${HOSTNAME}.mongo'))"
fi

if [[ "${HOSTNAME}" == 'mongo-0' ]]; then


echo "initialisation du ReplicaSet"
/usr/bin/mongo --eval="printjson(rs.initiate(\
{'_id': 'rs0', 'members': [{'_id': 0, \
'host': 'mongo-0.mongo:27017'}]}))"
fi
echo "initialized"

while true; do
sleep 3600
done

Ce script se met en veille après l’initialisation du cluster.


Chaque conteneur d’un pod doit avoir la même RestartPolicy.
Puisque nous ne voulons pas que notre conteneur principal
Mongo soit redémarré, il faut aussi que notre conteneur
d’initialisation soit exécuté en permanence, sinon Kubernetes
pourrait penser que notre pod Mongo est défaillant.

Si l’on regroupe tout cela, voici le StatefulSet complet qui utilise le


ConfigMap.
Exemple 13-13. mongo.yaml
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: mongo
spec:
serviceName: "mongo"
replicas: 3
template:
metadata:
labels:
app: mongo
spec:
containers:
- name: mongodb
image: mongo:3.4.1
command:
- mongod
- --replSet
- rs0
ports:
- containerPort: 27017
name: web
# Ce conteneur initialise mongodb, puis il se met en veille.
- name: init-mongo
image: mongo:3.4.1
command:
- bash
- /config/init.sh
volumeMounts:
- name: config
mountPath: /config
volumes:
- name: config
configMap:
name: "mongo-init"

Avec tous ces fichiers, vous pouvez créer un cluster Mongo en


utilisant les commandes suivantes :
$ kubectl apply -f mongo-config-map.yaml
$ kubectl apply -f mongo-service.yaml
$ kubectl apply -f mongo.yaml

Si vous préférez une autre méthode, vous pouvez tous les combiner
en un seul fichier YAML où les objets individuels sont séparés par --
-. Assurez-vous de conserver le même ordre, puisque la définition du
StatefulSet s’appuie sur la définition du ConfigMap existant.

13.3.4 Volumes persistants et StatefulSets

Pour le stockage persistant, vous devez monter un volume


persistant dans le répertoire/data/db. Dans le modèle de pod, vous
devez le mettre à jour pour monter une demande de volume
persistant dans ce répertoire :
...
volumeMounts:
- name: database
mountPath: /data/db
Bien que cette approche soit similaire à celle que nous avons vue
avec des singletons fiables, puisque le StatefulSet réplique plus d’un
pod, vous ne pouvez pas simplement référencer une demande de
volume persistant. Au lieu de cela, vous devez ajouter un modèle de
demande de volume persistant. Vous pouvez vous représenter le
modèle de demande comme étant identique au modèle de pod, mais
au lieu de créer des pods, il crée des demandes de volume. Vous
devez ajouter ce qui suit en bas de votre définition de StatefulSet :
volumeClaimTemplates:
- metadata:
name: database
annotations:
volume.alpha.kubernetes.io/storage-class: anything
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 100Gi

Lorsque vous ajoutez un modèle de demande de volume à une


définition de StatefulSet, chaque fois que le contrôleur des
StatefulSets crée un pod qui fait partie du StatefulSet, il crée une
demande de volume persistant basée sur ce modèle dans le cadre
de ce pod.

Pour que ces volumes persistants répliqués fonctionnent


correctement, vous devez disposer d’une configuration
provisionnée automatiquement pour les volumes persistants, ou
bien vous devez préremplir une collection d’objets de volumes
persistants pour que le contrôleur des StatefulSets s’en inspire.
Si aucune demande ne peut être créée, le contrôleur de
StatefulSets ne pourra pas créer les pods correspondants.

13.3.5 Un dernier mot sur les sondes de disponibilité


La dernière étape dans la production de notre cluster MongoDB
consiste à ajouter des contrôles d’activité à nos conteneurs Mongo.
Comme nous l’avons appris dans le chapitre 5, la sonde d’activité
est utilisée pour déterminer si un conteneur fonctionne correctement.
Pour les contrôles d’activité, nous pouvons utiliser l’outil mongo lui-
même en ajoutant les lignes suivantes au modèle de pod dans
l’objet StatefulSet :
...
livenessProbe:
exec:
command:
- /usr/bin/mongo
- --eval
- db.serverStatus()
initialDelaySeconds: 10
timeoutSeconds: 10
...

_ 13.4 RÉSUMÉ
Une fois que nous avons combiné les StatefulSets, les demandes de
volumes persistants, et les sondes d’activité, nous avons une
installation cloud-native, évolutive et renforcée de MongoDB
fonctionnant sur Kubernetes. Bien que cet exemple traite de
MongoDB, les étapes de création des StatefulSets pour gérer
d’autres solutions de stockage sont assez similaires et vous pouvez
suivre des modèles analogues.
14

Déploiement d’applications
réelles

Dans les chapitres précédents, nous avons décrit de nombreux


objets API qui sont disponibles dans un cluster Kubernetes et la
manière dont ces objets peuvent être utilisés pour construire des
systèmes distribués fiables. Cependant, aucun des chapitres
précédents n’a vraiment traité la façon dont vous pourriez utiliser ces
objets en pratique pour déployer une application complète et réelle.
C’est l’objet de ce chapitre.
Nous allons étudier trois applications réelles :
Parse, un serveur API open source pour les applications mobiles ;
Ghost, une plateforme de blogs et de gestion de contenu ;
Redis, un système de gestion de paires clé/valeur léger et performant.
Ces exemples complets devraient vous donner une meilleure idée
de la façon dont vous pouvez structurer vos propres déploiements à
l’aide de Kubernetes.

_ 14.1 PARSE
Le serveur Parse (https://parse.com) est une API cloud qui est
dédiée à la fourniture d’un stockage facile à utiliser pour les
applications mobiles. Il fournit une grande variété de bibliothèques
clientes qui le rendent facile à intégrer à Android, iOS et à d’autres
plateformes mobiles. Parse a été acheté par Facebook en 2013,
mais le produit a été arrêté par la suite. Heureusement pour nous,
un serveur compatible a été publié en open source par l’équipe de
développement de Parse, si bien que nous pouvons l’utiliser. Cette
section décrit comment configurer Parse dans Kubernetes.

14.1.1 Conditions préalables

Parse utilise un cluster MongoDB pour son stockage. Le chapitre 13


décrit comment configurer un cluster MongoDB répliqué à l’aide des
objets StatefulSets de Kubernetes. Cette section suppose que vous
avez un cluster Mongo avec trois réplicas en cours d’exécution dans
Kubernetes ayant les noms mongo-0.mongo, mongo-1.mongo et
mongo-2.mongo.
Nous partons aussi du principe que vous avez un identifiant Docker ;
si ce n’est pas le cas, vous pouvez en obtenir un gratuitement à
https://docker.com.
Enfin, nous supposons que vous avez un cluster Kubernetes
déployé et que l’outil kubectl est correctement configuré.

14.1.2 Création du serveur Parse

Le serveur open source Parse est livré avec un Dockerfile par


défaut, afin de faciliter la conteneurisation. Commencez par cloner le
dépôt Parse :
$ git clone https://github.com/ParsePlatform/parse-server

Ensuite, déplacez-vous dans ce répertoire et créez l’image :


$ cd parse-server
$ docker build -t ${DOCKER_USER}/parse-server .

Enfin, poussez cette image sur le hub Docker :


$ docker push ${DOCKER_USER}/parse-server
14.1.3 Déploiement du serveur Parse

Une fois que vous avez créé l’image du conteneur, le déploiement


du serveur Parse dans votre cluster est assez simple. Parse
recherche trois variables d’environnement lors de la configuration :
PARSE_SERVER_APPLICATION_ID
Un identificateur pour autoriser votre application
PARSE_SERVER_MASTER_KEY
Un identificateur qui autorise l’utilisateur principal (root)
PARSE_SERVER_DATABASE_URI
L’URI de votre cluster MongoDB
En regroupant tout cela, vous pouvez déployer Parse sous la forme
d’un déploiement Kubernetes à l’aide du fichier YAML de
l’exemple 14-1.
Exemple 14-1. parse.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: parse-server
namespace: default
spec:
replicas: 1
template:
metadata:
labels:
run: parse-server
spec:
containers:
- name: parse-server
image: ${DOCKER_USER}/parse-server
env:
- name: PARSE_SERVER_DATABASE_URI
value: "mongodb://mongo-0.mongo:27017,\
mongo-1.mongo:27017,mongo-2.mongo\
:27017/dev?replicaSet=rs0"
- name: PARSE_SERVER_APP_ID
value: my-app-id
- name: PARSE_SERVER_MASTER_KEY
value: my-master-key

14.1.4 Test de Parse

Pour tester votre déploiement, vous devez l’exposer en tant que


service Kubernetes. Vous pouvez le faire en utilisant la définition du
service de l’exemple 14-2.
Exemple 14-2. parse-service.yaml
apiVersion: v1
kind: Service
metadata:
name: parse-server
namespace: default
spec:
ports:
- port: 1337
protocol: TCP
targetPort: 1337
selector:
run: parse-server

Maintenant, votre serveur Parse est opérationnel et prêt à recevoir


les requêtes de vos applications mobiles. Bien sûr, dans toute
application réelle, vous allez probablement vouloir sécuriser la
connexion avec HTTPS. Vous pouvez consulter la page GitHub du
projet parse-server (https://github.com/parse-community/parse-
server) pour de plus amples informations sur une telle configuration.

_ 14.2 GHOST
Ghost est un moteur de blogging populaire avec une interface
conviviale écrite en JavaScript. Il peut utiliser une base de données
SQLite basée sur des fichiers ou bien MySQL pour le stockage.

14.2.1 Configuration de Ghost

Ghost est configuré avec un simple fichier JavaScript qui décrit le


serveur. Nous allons stocker ce fichier en tant qu’objet ConfigMap.
L’exemple 14-3 illustre une configuration de développement simple
pour Ghost.
Exemple 14-3. ghost-config.js
var path = require('path'),
config;

config = {
development: {
url: 'http://localhost:2368',
database: {
client: 'sqlite3',
connection: {
filename: path.join(process.env.GHOST_CONTENT,
'/data/ghost-dev.db')
},
debug: false
},
server: {
host: '0.0.0.0',
port: '2368'
},
paths: {
contentPath: path.join(process.env.GHOST_CONTENT, '/')
}
}
};

module.exports = config;
Une fois que vous avez enregistré ce fichier de configuration ghost-
config.js, vous pouvez créer un objet Kubernetes ConfigMap avec la
commande :
$ kubectl create cm --from-file ghost-config.js ghost-config

Cela crée un ConfigMap nommé ghost-config. Comme avec


l’exemple Parse, nous allons monter ce fichier de configuration en
tant que volume à l’intérieur de notre conteneur. Nous allons
déployer Ghost en tant qu’objet Deployment, qui définit ce montage
de volume dans le cadre du modèle de pod (exemple 14-4).
Exemple 14-4. ghost.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: ghost

spec:
replicas: 1
selector:
matchLabels:
run: ghost
template:
metadata:
labels:
run: ghost
spec:
containers:
- image: ghost
name: ghost
command:
- sh
- -c
- cp /ghost-config/ghost-config.js /var/lib/ghost/config.js
&& /usr/local/bin/docker-entrypoint.sh node current/index.js
volumeMounts:
- mountPath: /ghost-config
name: config
volumes:
- name: config
configMap:
defaultMode: 420
name: ghost-config

Il faut noter une chose ici : nous copions le fichier config.js à partir
d’un emplacement différent de l’emplacement où Ghost s’attend à le
trouver, puisque le ConfigMap ne peut monter que des répertoires,
et non pas des fichiers individuels. Ghost attend la présence d’autres
fichiers dans son répertoire qui ne sont pas dans ce ConfigMap, si
bien que nous ne pouvons pas monter le ConfigMap entier dans
/var/lib/ghost.
Vous pouvez exécuter ceci avec la commande :
$ kubectl apply -f ghost.yaml

Une fois que le pod est fonctionnel, vous pouvez l’exposer en tant
que service avec :
$ kubectl expose deployments ghost --port=2368

Une fois le service exposé, vous pouvez utiliser la commande


kubectl proxy pour accéder au serveur Ghost :
$ kubectl proxy

Allez avec votre navigateur Web sur la page


http://localhost:8001/api/v1/namespaces/default/services/ghost/proxy
/ pour commencer à interagir avec Ghost.

◆ Ghost + MySQL

Bien entendu, cet exemple n’est pas très évolutif, ni même fiable,
puisque le contenu du blog est stocké dans un fichier local à
l’intérieur du conteneur. Une approche plus évolutive consiste à
stocker les données du blog dans une base de données MySQL.
Pour ce faire, modifiez d’abord config.js pour inclure :
...
database: {
client: 'mysql',
connection: {
host : 'mysql',
user : 'root',
password : 'root',
database : 'ghost_db',
charset : 'utf8'
}
},
...

Ensuite, créez un nouvel objet ConfigMap ghost-config :


$ kubectl create configmap ghost-config-mysql --from-file ghost-config.js

Ensuite, mettez à jour le déploiement Ghost pour changer le nom du


ConfigMap monté à partir de config-map en config-map-mysql :
...
- configMap:
name: ghost-config-mysql
...

En utilisant les instructions de la section « Stockage natif Kubernetes


avec des StatefulSets » du chapitre précédent, déployez un serveur
MySQL dans votre cluster Kubernetes. Assurez-vous qu’un service
nommé mysql est également défini.
Vous allez devoir créer la base de données dans la base de
données MySQL :
$ kubectl exec -it mysql-zzmlw -- mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
...

mysql> create database ghost_db;


...

Pour finir, effectuez un déploiement de cette nouvelle configuration.


$ kubectl apply -f ghost.yaml

Étant donné que votre serveur Ghost est maintenant découplé de sa


base de données, vous pouvez redimensionner votre serveur Ghost
et il continuera à partager les données entre tous les réplicas.
Modifiez le fichier ghost.yaml pour définir spec.replicas à 3, puis
exécutez :
$ kubectl apply -f ghost.yaml

Votre installation Ghost est maintenant dotée de trois réplicas.

_ 14.3 REDIS
Redis est un système de gestion de paires clé/valeur en mémoire
très populaire, avec de nombreuses fonctionnalités supplémentaires.
C’est une application intéressante à déployer car c’est un bon
exemple de la valeur de l’abstraction des pods Kubernetes. Ceci est
dû au fait qu’une installation fiable de Redis consiste en fait en deux
programmes fonctionnant ensemble. Le premier est redis-server, qui
implémente le système de paires clé/valeur, et l’autre est redis-
sentinel, qui implémente la vérification de l’intégrité et le
basculement vers un cluster Redis répliqué.
Lorsque Redis est déployé de manière répliquée, il existe un serveur
maître unique qui peut être utilisé pour les opérations de lecture et
d’écriture. En outre, il existe d’autres serveurs de réplicas qui
dupliquent les données écrites sur le serveur maître et qui peuvent
être utilisés pour l’équilibrage de charge des opérations de lecture.
N’importe lequel de ces réplicas peut basculer pour devenir le
serveur maître si le maître original tombe en panne. Ce basculement
est effectué par la sentinelle de Redis. Dans notre déploiement, le
serveur Redis et sa sentinelle sont hébergés dans le même fichier.

14.3.1 Configuration de Redis


Comme nous l’avons déjà fait, nous allons utiliser les objets
ConfigMap de Kubernetes pour configurer notre installation de
Redis. Redis a besoin de configurations séparées pour les réplicas
maître et esclave. Pour configurer le maître, créez un fichier nommé
master.conf qui contient le code de l’exemple 14-5.
Exemple 14-5. master.conf
bind 0.0.0.0
port 6379

dir /redis-data

Ce fichier ordonne à Redis de se lier à toutes les interfaces réseau


sur le port 6379 (le port Redis par défaut) et de stocker ses fichiers
dans le répertoire /redis-data.
La configuration esclave est identique, mais elle ajoute une directive
slaveof. Créez un fichier nommé slave.conf qui contient les données
de l’exemple 14-6.
Exemple 14-6. slave.conf
bind 0.0.0.0
port 6379

dir .

slaveof redis-0.redis 6379

Notez que nous utilisons redis-0.redis pour le nom du maître. Nous


allons configurer ce nom à l’aide d’un service et d’un StatefulSet.
Nous avons aussi besoin d’une configuration pour la sentinelle
Redis. Créez un fichier nommé sentinel.conf avec le contenu de
l’exemple 14-7.
Exemple 14-7. sentinel.conf
bind 0.0.0.0
port 26379

sentinel monitor redis redis-0.redis 6379 2


sentinel parallel-syncs redis 1
sentinel down-after-milliseconds redis 10000
sentinel failover-timeout redis 20000

Maintenant que nous avons tous nos fichiers de configuration, nous


avons besoin de créer deux scripts simples à utiliser dans notre
déploiement du StatefulSet.
Le premier script regarde simplement le nom d’hôte du pod et
détermine s’il s’agit du maître ou de l’esclave, puis il lance Redis
avec la configuration appropriée. Créez un fichier nommé init.sh
contenant le code de l’exemple 14-8.
Exemple 14-8. init.sh
#!/bin/bash
if [[ ${HOSTNAME} == 'redis-0' ]]; then
redis-server /redis-config/master.conf
else
redis-server /redis-config/slave.conf
fi

L’autre script est pour la sentinelle. Dans ce cas, il est nécessaire


parce que nous devons attendre que le nom DNS redis-0.redis
devienne disponible. Créez un script nommé sentinel.sh contenant le
code de l’exemple 14-9.
Exemple 14-9. sentinel.sh
#!/bin/bash
cp /redis-config-src/*.* /redis-config
while ! ping -c 1 redis-0.redis; do
echo 'Attente du serveur'
sleep 1
done

redis-sentinel /redis-config/sentinel.conf

Maintenant, nous devons créer un package de tous ces fichiers dans


un objet ConfigMap. Vous pouvez faire cela avec une seule ligne de
commande :
$ kubectl create configmap \
--from-file=slave.conf=./slave.conf \
--from-file=master.conf=./master.conf \
--from-file=sentinel.conf=./sentinel.conf \
--from-file=init.sh=./init.sh \
--from-file=sentinel.sh=./sentinel.sh \
redis-config

14.3.2 Création d’un service Redis

L’étape suivante du déploiement de Redis consiste à créer un


service Kubernetes qui fournira le nommage et la découverte des
réplicas Redis (par exemple, redis-0.redis). Pour ce faire, nous
créons un service sans adresse IP de cluster (exemple 14-10).
Exemple 14-10. redis-service.yaml
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
ports:
- port: 6379
name: peer
clusterIP: None
selector:
app: redis

Vous pouvez créer ce service avec kubectl apply -f redis-


service.yaml. Ne vous inquiétez pas du fait que les pods du service
n’existent pas encore. Kubernetes ne s’en soucie pas et il ajoutera
les bons noms lorsque les pods seront créés.

14.3.3 Déploiement de Redis

Nous sommes prêts à déployer notre cluster Redis. Pour ce faire,


nous allons utiliser un StatefulSet. Nous avons présenté les
StatefulSets au chapitre précédent quand nous avons discuté de
notre installation MongoDB. Les StatefulSets créent des indices (par
exemple, redis-0.redis), et offrent aussi une sémantique de création
et de suppression ordonnée (redis-0 sera toujours créé avant redis-
1, et ainsi de suite). Les StatefulSets sont très utiles pour les
applications stateful comme Redis, mais honnêtement, ils
ressemblent beaucoup aux objets Deployment de Kubernetes. Pour
notre cluster Redis, voici à quoi ressemble le StatefulSet illustré à
l’exemple 14-11.
Exemple 14-11. redis.yaml
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: redis
spec:
replicas: 3
serviceName: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- command: [sh, -c, source /redis-config/init.sh ]
image: redis:4.0.11-alpine
name: redis
ports:
- containerPort: 6379
name: redis
volumeMounts:
- mountPath: /redis-config
name: config
- mountPath: /redis-data
name: data
- command: [sh, -c, source /redis-config-src/sentinel.sh]
image: redis:4.0.11-alpine
name: sentinel
volumeMounts:
- mountPath: /redis-config-src
name: config
- mountPath: /redis-config
name: data
volumes:
- configMap:
defaultMode: 420
name: redis-config
name: config
- emptyDir:
name: data
volumeMounts:
- mountPath: /redis-config
name: config
- mountPath: /redis-date
name: data
- command: [sh, -c, source /redis-config/sentinel.sh]
image: redis:3.2.7-alpine
name: sentinel
volumeMounts:
- mountPath: /redis-config
name: config

Vous pouvez voir qu’il y a deux conteneurs dans ce pod. L’un


exécute le script init.sh que nous avons créé et le serveur principal
Redis ; l’autre est la sentinelle qui surveille les serveurs.
Vous pouvez également noter qu’il y a deux volumes définis dans le
pod. L’un est le volume qui utilise notre ConfigMap pour configurer
les deux applications Redis, et l’autre est un simple volume emptyDir
qui est mappé dans le conteneur du serveur Redis pour contenir les
données de l’application, de telle sorte qu’il puisse survivre à un
redémarrage du conteneur. Pour une installation plus fiable de
Redis, il pourrait s’agir d’un disque connecté au réseau, comme cela
a été évoqué dans le chapitre précédent.
Maintenant que nous avons défini notre cluster Redis, nous pouvons
le créer avec la commande suivante :
$ kubectl apply -f redis.yaml

14.3.4 Test de notre cluster Redis

Pour faire la démonstration que nous avons réellement réussi à


créer un cluster Redis, nous pouvons effectuer quelques tests.
Tout d’abord, nous pouvons déterminer quel serveur apparaît
comme maître pour la sentinelle Redis. Pour ce faire, nous pouvons
exécuter la commande redis-cli dans l’un des pods :
$ kubectl exec redis-2 -c redis \
-- redis-cli -p 26379 sentinel get-master-addr-by-name redis

Cela devrait afficher l’adresse IP du pod redis-0. Vous pouvez


confirmer cette information en utilisant kubectl get pods -o wide.
Ensuite, nous allons vérifier que la réplication fonctionne réellement.
Pour ce faire, essayez d’abord de lire la valeur foo à partir de l’un
des réplicas :
$ kubectl exec redis-2 -c redis -- redis-cli -p 6379 get foo

Vous ne devriez voir aucune donnée s’afficher.


Ensuite, essayez d’écrire ces données dans un réplica :
$ kubectl exec redis-2 -c redis -- redis-cli -p 6379 set foo 10
READONLY You can’t write against a read only slave.

Vous ne pouvez pas écrire dans un réplica, car il est en lecture


seule. Essayons la même commande sur redis-0, qui est le maître :
$ kubectl exec redis-0 -c redis -- redis-cli -p 6379 set foo 10
OK

Maintenant, essayez la commande précédente de lecture à partir


d’un réplica :
$ kubectl exec redis-2 -c redis -- redis-cli -p 6379 get foo
10
Cela montre que notre cluster est correctement configuré et que les
données sont répliquées entre les maîtres et les esclaves.

_ 14.4 RÉSUMÉ
Dans les sections précédentes, nous avons décrit comment déployer
une grande variété d’applications en utilisant différents concepts
Kubernetes. Nous avons vu comment mettre en place des services
de nommage et de découverte pour déployer des interfaces Web
comme Ghost, ainsi que des serveurs API comme Parse. Nous
avons aussi vu comment l’abstraction des pods facilite le
déploiement des composants qui constituent un cluster Redis fiable.
Même si vous ne souhaitez pas réellement déployer ces applications
en production, ces exemples illustrent des modèles que vous pouvez
imiter pour gérer vos applications à l’aide de Kubernetes. Nous
espérons que la mise en pratique des concepts que nous avons
décrits dans les chapitres précédents dans des exemples tirés du
monde réel vous aura aidé à mieux comprendre comment exploiter
Kubernetes.
ANNEXE
Construction d’un cluster
Kubernetes
avec des Raspberry Pi

La plupart du temps, Kubernetes est mis en œuvre par le biais d’un


fournisseur de cloud public et vous n’avez aucune proximité avec
votre cluster puisque vous ne le voyez qu’à travers un navigateur
Web ou un terminal. Il peut cependant être très intéressant de
construire physiquement un cluster Kubernetes avec ses propres
machines. De cette manière, il n’y a rien de mieux pour vous
convaincre de l’utilité de Kubernetes que de couper le courant sur un
nœud pour voir comment l’orchestrateur réagit à cette panne et
soigne votre application.
Construire votre propre cluster peut vous paraître à la fois très
complexe et ruineux, mais heureusement, il n’en est rien. La
possibilité d’acheter des nano-ordinateurs bon marché basés sur un
SOC, ainsi que le travail énorme accompli par la communauté pour
rendre Kubernetes plus facile à installer, signifie qu’il est possible de
construire un petit cluster Kubernetes en quelques heures.
Dans les instructions suivantes, nous nous concentrons sur la
construction d’un cluster composé de Raspberry Pi, mais avec de
légères adaptations, les mêmes instructions peuvent servir à
travailler avec de nombreux autres nano-ordinateurs.
_ LISTE DE COURSES
La première chose que vous devez faire consiste à assembler les
pièces de votre cluster. Dans tous les exemples de cette annexe,
nous supposons que notre cluster comporte quatre nœuds. Vous
pouvez construire un cluster de trois nœuds, ou même un cluster de
cent nœuds si vous le souhaitez, mais quatre nœuds nous semblent
un bon compromis.
Pour commencer, vous aurez besoin d’acheter les différents
éléments nécessaires à la construction du cluster. Voici la liste de
courses, avec les prix approximatifs au moment de la rédaction de
cet ouvrage :
1. Quatre Raspberry Pi 3 (le Raspberry Pi 2 fonctionne
également) : 160 €
2. Quatre cartes mémoire SDHC, d’au moins 8 Go (ne lésinez pas
sur la qualité) : de 30 à 50 €
3. Quatre câbles Ethernet de catégorie 6 de 0,5 mètre : 10 €
4. Quatre câbles micro-USB A de 0,5 mètre : 10 €
5. Un switch Fast Ethernet 5 ports 10/100 : 10 €
6. Un chargeur USB 5 ports : 25 €
7. Un boîtier permettant d’empiler quatre Raspberry Pi : 40 € (vous
pouvez aussi le construire vous-même)
8. Un adaptateur USB-jack pour alimenter le switch Ethernet
(optionnel) : 5 €
Le prix total de ce cluster est d’environ 300 euros, mais vous pouvez
encore baisser le prix jusqu’à 200 euros en construisant un cluster à
trois nœuds et en supprimant le boîtier et le câble d’alimentation
USB pour le switch (même si le boîtier et le câble donnent un aspect
soigné à l’ensemble du cluster).
Nous insistons sur le fait qu’il ne faut pas lésiner sur la qualité des
cartes mémoire SD. Les cartes mémoire bas de gamme se
comportent de manière imprévisible et votre cluster devient vraiment
instable. Si vous voulez économiser de l’argent, achetez une carte
de taille plus petite, mais de bonne qualité. On trouve en ligne des
cartes de 8 Go fiables pour environ 7 euros.
Quoi qu’il en soit, une fois que vous avez rassemblé tous les
éléments, vous êtes prêt à passer à la construction du cluster.
Ces instructions supposent également que vous avez un dispositif
capable d’écrire sur une carte SDHC. Si ce n’est pas le cas, vous
devrez acheter un lecteur USB de carte mémoire.

_ GÉNÉRATION DES IMAGES


L’image par défaut Raspbian prend désormais en charge Docker via
les méthodes d’installation standard, mais pour rendre les choses
encore plus faciles, le projet Hypriot (http://hypriot.com) fournit des
images avec Docker préinstallé.
Visitez la page de téléchargement
(http://blog.hypriot.com/downloads/) du projet et téléchargez la
dernière image stable. Décompressez l’image, et vous devriez
maintenant avoir un fichier .img. Le projet Hypriot fournit également
une excellente documentation pour écrire cette image sur votre carte
mémoire :
Mac OS (http://bit.ly/hypriot-docker)
Windows (http://bit.ly/hypriot-windows)
Linux (http://bit.ly/hypriot-linux)
Écrivez la même image sur chacune de vos cartes mémoire.

_ PREMIER DÉMARRAGE : NŒUD


MASTER
La première chose à faire est de démarrer uniquement votre nœud
master. Assemblez votre cluster, et décidez quelle machine sera le
nœud maître. Insérez la carte mémoire, branchez le Raspberry Pi à
un écran avec un câble HDMI et branchez un clavier à l’un des ports
USB.
Ensuite, branchez une alimentation pour démarrer la carte.
Quand l’invite s’affiche, connectez-vous avec le nom d’utilisateur
pirate et le mot de passe hypriot.

La première chose que vous devez faire avec votre


Raspberry Pi (ou tout nouvel ordinateur) est de changer le mot
de passe par défaut. Le mot de passe par défaut de chaque
type d’installation est parfaitement connu par les personnes qui
cherchent à pirater des systèmes. Cela rend l’Internet moins sûr
pour tout le monde. S’il vous plaît, changez vos mots de passe
par défaut !

Configuration de la mise en réseau

L’étape suivante consiste à configurer le réseau sur le maître.


Commencez par installer le Wi-Fi qui sera le lien entre votre cluster
et le monde extérieur. Modifiez le fichier /boot/device-init.yaml.
Mettez à jour le SSID et le mot de passe Wi-Fi pour qu’ils
correspondent à votre environnement. Si jamais vous voulez
changer de réseau, c’est ce fichier qu’il faudra modifier. Une fois que
vous avez effectué les modifications, redémarrez avec sudo reboot
et vérifiez que votre réseau fonctionne.
La prochaine étape de la mise en réseau consiste à configurer une
adresse IP statique pour le réseau interne de votre cluster. Pour ce
faire, ouvrez le fichier /etc/network/interfaces.d/eth0 et modifiez les
lignes suivantes :
allow-hotplug eth0
iface eth0 inet static
address 10.0.0.1
netmask 255.255.255.0
broadcast 10.0.0.255
gateway 10.0.0.1

Cela alloue de manière statique l’adresse 10.0.0.1 à l’interface


Ethernet principale.
Redémarrez la machine pour obtenir cette adresse.
Ensuite, nous allons installer le DHCP sur ce nœud maître afin qu’il
alloue des adresses aux nœuds worker. Exécutez la commande
suivante :
$ apt-get install isc-dhcp-server

Ensuite, configurez le serveur DHCP de la manière suivante


(/etc/dhcp/dhcpd.conf) :
# Définit un nom de domaine ; vous pouvez choisir n’importe quel nom
option domain-name "cluster.home";

# Utilise le DNS Google par défaut, mais vous pouvez remplacer cette valeur
# par le DNS de votre fournisseur d’accès à Internet
option domain-name-servers 8.8.8.8, 8.8.4.4;

# On utilise 10.0.0.X pour notre sous-réseau


subnet 10.0.0.0 netmask 255.255.255.0 {
range 10.0.0.1 10.0.0.10;
option subnet-mask 255.255.255.0;
option broadcast-address 10.0.0.255;
option routers 10.0.0.1;
}
default-lease-time 600;
max-lease-time 7200;
authoritative;

Vous aurez aussi peut-être besoin de modifier /etc/defaults/isc-dhcp-


server pour définir la variable d’environnement INTERFACES à eth0.
Redémarrez le serveur DHCP avec sudo systemctl restart isc-dhcp-
server.
Votre machine doit maintenant distribuer des adresses IP. Vous
pouvez tester cela en connectant une deuxième machine au switch
Ethernet. Cette deuxième machine doit obtenir l’adresse 10.0.0.2 à
partir du serveur DHCP.
N’oubliez pas de modifier le fichier /boot/device-init.yaml pour
renommer cette machine en node-1.
La dernière étape de la mise en réseau est la configuration de la
traduction d’adresses réseau (NAT) afin que vos nœuds puissent
atteindre l’Internet public (si vous voulez qu’ils puissent le faire).
Modifiez le fichier /etc/sysctl.conf et définissez net.ipv4.ip_forward=1
pour activer le transfert d’adresses IP.
Puis éditez le fichier /etc/rc.local (ou son équivalent) et ajoutez des
règles iptables pour le transfert de eth0 vers wlan0 (et inversement) :
$ iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE
$ iptables -A FORWARD -i wlan0 -o eth0 -m state \
--state RELATED,ESTABLISHED -j ACCEPT
$ iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT

À ce stade, la configuration réseau de base doit être terminée.


Branchez et allumez les deux autres Raspberry Pi (à qui les
adresses 10.0.0.3 et 10.0.0.4 doivent être assignées). Modifier le
fichier /boot/device-init.yaml sur chaque machine pour les renommer
respectivement node-2 et node-3.
Vérifiez cela en regardant d’abord /var/lib/dhcp/dhcpd.leases et puis
faites une connexion SSH sur les nœuds (rappelez-vous que la
première chose à faire est de changer le mot de passe par défaut).
Vérifiez que les nœuds peuvent se connecter à l’Internet public.

◆ Opérations supplémentaires

Il y a encore quelques opérations que vous pouvez accomplir pour


faciliter la gestion du réseau de votre cluster.
La première est de modifier /etc/hosts sur chaque machine pour
mapper les noms sur les bonnes adresses. Sur chaque machine,
ajoutez :
...
10.0.0.1 kubernetes
10.0.0.2 node-1
10.0.0.3 node-2
10.0.0.4 node-3
...

Maintenant, vous pouvez utiliser ces noms lors de la connexion à


ces machines.
La seconde est de mettre en place un accès SSH sans mot de
passe. Pour ce faire, exécutez ssh-keygen puis copiez le fichier
$HOME/.ssh/id_rsa.pub dans /home/pirate/.ssh/authorized_keys sur
node-1, node-2 et node-3.

Installation de Kubernetes

À ce stade, vous devriez avoir tous les nœuds opérationnels, avec


des adresses IP et un accès à Internet. C’est le moment d’installer
Kubernetes sur tous les nœuds.
À l’aide de SSH, exécutez les commandes suivantes sur tous les
nœuds pour installer les outils kubelet et kubeadm. Vous devez avoir
un accès root pour les commandes suivantes. Utilisez sudo su pour
élever vos privilèges à ceux de l’utilisateur root.
Tout d’abord, ajoutez la clé de cryptage pour les packages :
# curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -

Ajoutez ensuite le référentiel à votre liste de dépôts :


# echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" \
>> /etc/apt/sources.list.d/kubernetes.list

Enfin, faites une mise à jour et installez les outils Kubernetes. Cela
va également mettre à niveau tous les paquets de votre système :
# apt-get update
$ apt-get upgrade
$ apt-get install -y kubelet kubeadm kubectl kubernetes-cni
Configuration du cluster

Sur le nœud master (celui qui exécute le DHCP et qui est connecté
à Internet), exécutez :
$ sudo kubeadm init --pod-network-cidr 10.244.0.0/16 \
--apiserver-advertise-address 10.0.0.1 \
--apiserver-cert-extra-sans kubernetes.cluster.home

Notez que vous diffusez votre adresse IP interne, et non pas votre
adresse externe.
En fin de compte, cela affichera une commande pour joindre les
nœuds à votre cluster. Cela devrait ressembler à quelque chose
similaire à ce qui suit :
$ kubeadm join --token=<token> 10.0.0.1

Connectez-vous en SSH à chacun des nœuds worker de votre


cluster et exécutez cette commande.
Lorsque tout cela est terminé, vous devez être en mesure de
bénéficier d’un cluster opérationnel :
$ kubectl get nodes

◆ Configuration de la mise en réseau des clusters

Vous disposez de votre configuration réseau au niveau du nœud,


mais vous devez configurer le réseau pod à pod. Étant donné que
tous les nœuds de votre cluster s’exécutent sur le même réseau
physique Ethernet, il vous suffit de configurer les bonnes règles de
routage dans les noyaux hôtes.
La meilleure façon de gérer cela est d’utiliser l’outil Flanel
(http://bit.ly/2vgBsKU) créé par CoreOs. Flanel supporte plusieurs
modes de routage différents et nous allons utiliser le mode host-gw.
Vous pouvez télécharger un exemple de configuration à partir de la
page du projet Flanel (https://github.com/coreos/flannel) :
$ curl https://rawgit.com/coreos/flannel/master/Documentation/kube-flannel.yml \
> kube-flannel.yaml
La configuration par défaut fournie par CoreOs utilise le mode vxlan
à la place, et utilise également l’architecture AMD64 au lieu de
l’architecture ARM. Pour résoudre ce problème, ouvrez ce fichier de
configuration dans votre éditeur préféré ; remplacez vxlan par host-
gw et remplacez toutes les occurrences de amd64 par arm.
Vous pouvez également faire cela avec l’outil sed :
$ curl https://rawgit.com/coreos/flannel/master/Documentation/kube-flannel.yml \
| sed "s/amd64/arm/g" | sed "s/vxlan/host-gw/g" \
> kube-flannel.yaml

Une fois que vous avez mis à jour le fichier kube-flannel.yaml, vous
pouvez créer la configuration réseau de Flanel avec :
$ kubectl apply -f kube-flannel.yaml

Cela va créer deux objets : un ConfigMap utilisé pour configurer


Flanel et un DaemonSet qui gère le démon de Flanel. Vous pouvez
vérifier cela avec les commandes suivantes :
$ kubectl describe --namespace=kube-system configmaps/kube-flannel-cfg
$ kubectl describe --namespace=kube-system daemonsets/kube-flannel-ds

◆ Configuration de l’interface graphique

Kubernetes est livré avec une riche interface graphique. Vous


pouvez l’installer en exécutant :
$ DASHSRC=https://raw.githubusercontent.com/kubernetes/dashboard/master
$ curl -sSL \
$DASHSRC/src/deploy/recommended/kubernetes-dashboard-arm-head.yaml \
| kubectl apply -f -

Pour accéder à cette interface utilisateur, vous pouvez exécuter


kubectl proxy puis pointer votre navigateur sur
http://localhost:8001/ui où localhost est local par rapport au nœud
master de votre cluster. Pour afficher cela à partir de votre
ordinateur, vous devrez peut-être configurer un tunnel SSH vers le
nœud racine en utilisant ssh -L8001:localhost:8001 <adresse-ip-
master>.
_ RÉSUMÉ
À ce stade, vous devriez avoir un cluster Kubernetes opérationnel
sur vos Raspberry Pi, ce qui est parfait pour découvrir Kubernetes.
Ordonnancez des jobs, ouvrez l’interface utilisateur et essayez de
malmener votre cluster en redémarrant les machines ou en
débranchant le réseau.
Index

abstraction 1
Alpine 1
Amazon Web Services 1
annotation 1
création 1
utilisation 1
API 1
sélecteur d’étiquettes 1
application
évolution 1
exemple 1
exemple réel 1
mobile stockage 1
apply 1
auto-guérison 1
auto-réparation 1
Azure Cloud 1
base de données
MySQL 1
blog
application Ghost 1
boucle de rapprochement 1, 2
cgroups 1
cloud
intégration 1
cluster
composants 1, 2
créé à partir de Raspberry Pi 1
dépassement des limites 1
déploiement 1
évolution 1
MongoDB 1
provisionnement dynamique du volume 1
variables d’environnement 1
vérification de l'état 1
code
exemples du livre 1
completions 1, 2
ConfigMap 1
contraintes de nommage 1
création 1, 2
gestion 1
liste 1
mise à jour 1
utilisation 1
configuration déclarative 1
conteneur
catégories 1
contrôles d'intégrité 1
copie de fichiers 1
création d'image 1
d’application 1
débogage 1
exécution de commandes 1
format d’image Docker 1
image 1
logs 1, 2
mise à jour de l'image 1
mise en quarantaine 1
optimisation de la taille de l'image 1
registre distant 1
registre privé 1
registre public 1
sécurité des images 1
stockage distant de l'image 1
stratification 1
suppression d'une image 1
système 1
contexte 1
DaemonSet 1
ajout d’étiquettes aux nœuds 1
création 1
déploiement des mises à jour 1
limitation à certains nœuds 1
mise à jour 1
ordonnanceur 1
sélecteurs de nœuds 1
suppression 1
suppression des pods individuels 1
débogage 1
découplage 1, 2, 3
découverte des services 1
déploiement 1
création 1
cycle de vie 1
exemple 1
exemples d'applications réelles 1
gestion 1
historique 1
informations détaillées 1
mise à jour 1
mise à l’échelle 1
ralentissement 1
stratégies 1
suppression 1
Deployment 1
fonctionnement interne 1
développement
évolutivité 1
rapidité 1
disponibilité
vérification 1
DNS 1, 2, 3
Docker 1
exécution de conteneur 1
format d’image 1
limitation des ressources 1
registres privés 1
runtime 1
Dockerfile 1
données persistantes 1
disques distants 1
edit 1
Endpoints 1
équilibrage de charge 1, 2, 3, 4
équipe
évolutivité 1
espace de noms 1, 2
état actuel 1
état désiré 1
état observé 1
etcd 1
étiquette 1, 2
création 1
modification 1
origine 1
sélecteurs 1
syntaxe 1
évolutivité 1
exemples de code
téléchargement 1
gcloud 1
Ghost 1
configuration 1
MySQL 1
Google Cloud Platform 1
Google Container Engine 1
Google Container Registry 1
HPA 1
Hypriot 1
immutabilité 1
infrastructure
abstraction 1
infrastructure immutable 1
infrastructure mutable 1
intégrité
contrôles 1
types de contrôles 1
interface utilisateur 1
job 1
chargement d’une file d’attente 1
création 1
démarrage d’une file d’attente 1
file d'attente 1
modèles 1
ponctuel 1
JSON 1
JSONPath 1
kuard
exemple d’application 1
interface Web 1
kubectl 1
commandes courantes 1
kube-proxy 1
Kubernetes
abstraction de l’infrastructure 1
affichage des objets 1
API 1
client 1
cluster avec des Raspberry Pi 1
création des objets 1
découplage 1
demande d’API 1
déploiement 1
déploiement de cluster 1
description 1
DNS 1
efficacité 1
évolutivité 1
importation de services externes 1
installation en local 1
installation sur Amazon Web Services 1
installation sur Google Container Engine 1
installation sur Microsoft Azure 1
installation sur un Raspberry Pi 1
interface utilisateur 1
isolation des problèmes 1
modèles d'utilisation de volumes 1
modification d'un objet 1
objet ConfigMap 1
objet DaemonSet 1
objet Deployment 1
objet Endpoints 1
objet Job 1
objet ReplicaSet 1
objet Service 1
origine 1
pods 1
proxy 1
runtime de conteneur par défaut 1
solutions de stockage 1
stockage natif 1
suppression d'un objet 1
vitesse 1
Linux
distribution minimaliste 1
LoadBalancer 1
manifeste 1
métadonnées 1, 2
métrique d’utilisation 1
micro-service 1
Microsoft Azure 1
minikube 1, 2
MongoDB 1
automatisation du déploiement 1
MySQL 1
exécution dans un pod 1
Ghost 1
nœud
liste 1
nœud master 1
nœud worker 1
Open Container Initiative 1
ordonnancement 1
ordonnanceur 1
requêtes 1
parallelism 1, 2
parallélisme 1
Parse 1
création du serveur 1
déploiement du serveur 1
test 1
PersistentVolumeClaim 1
PersistentVolumes 1
planificateur Voir ordonnanceur 1
pod 1
accès 1
création 1
création d'un manifeste 1
défaillance 1
exécution 1
informations détaillées 1
liste 1
manifeste 1
modèle 1
MySQL 1
rapport avec les ReplicaSets 1
suppression 1
utilisation de volumes 1
utilité 1
point de terminaison 1
proxy 1
Raspberry Pi 1
configuration du cluster 1
création d'un cluster 1
génération des images 1
installation de Kubernetes 1
Raspbian 1
Recreate 1
redirection de port 1
Redis 1
configuration 1
création d’un service 1
déploiement 1
sentinelle 1
test du cluster 1
ReplicaSet 1
création 1
emploi 1
inspection 1
mise à l’échelle automatique 1
mise à l’échelle basée sur le CPU 1
mise à l'échelle déclarative 1
mise à l'échelle impérative 1
rapport avec les pods 1
recherche à partir d’un pod 1
recherche de pods 1
spécifications 1
suppression 1
ressources
gestion 1
limitation de la mémoire 1
limitation du CPU 1
métriques 1
minimales requises 1
plafonnement 1
séparation 1
RESTful 1
RollingUpdate 1
configuration d’une stratégie 1
scale 1
secret 1
contraintes de nommage 1
création 1, 2
d’extraction d’image 1
gestion 1
liste 1
mise à jour 1
utilisation 1
Service 1
service
découverte 1
découverte manuelle 1
évolution 1
gestion des versions multiples 1
sans sélecteurs 1
singleton fiable
exécution 1
SLA 1
sonde d'activité 1
sonde de disponibilité 1, 2
stateful 1, 2, 3, 4
StatefulSet 1
MongoDB 1
propriétés 1
volumes persistants 1
stateless 1, 2, 3, 4, 5
stockage persistant 1
stratégie
déploiements 1
système distribué 1
variable d’environnement 1
volume
persistance des données 1
provisionnement dynamique 1
volume de secrets 1
YAML 1
1. Brendan Burns et al., “Borg, Omega, and Kubernetes: Lessons Learned from Three
Container-Management Systems over a Decade,” ACM Queue 14 (2016): 70–93,
disponible à http://bit.ly/2vIrL4S.
2. NdT : on attribue cette expression à Jeff Bezos, le fondateur d’Amazon.
3. NdT : depuis la parution de l’édition originale de cet ouvrage, Amazon propose ce
service. Vous trouverez de plus amples informations sur
https://aws.amazon.com/fr/kubernetes/.
4. Comme vous l’apprendrez dans le chapitre suivant, un espace de noms dans
Kubernetes est une entité pour l’organisation des ressources Kubernetes. Vous pouvez
vous le représenter comme un dossier dans un système de fichiers.
5. Il s’agit d’une syntaxe YAML compacte : un élément d’une liste (matchExpressions)
est un mappage avec trois entrées. La dernière entrée (values) a une valeur qui est une
liste avec deux éléments.
6. Pour plus de précision voir https://www.tableau.com/fr-fr/about/blog/2017/3/what-
data-gravity-anyway-67725

Vous aimerez peut-être aussi