Mettre en place sa stratégie de migration vers Kubernetes

Article co-rédigé par Jérôme Thin, Wael Amri et Adnan El Akkaoui
Tout le monde veut que son application s’exécute sur Kubernetes. Mais comment démarrer ce processus ? Quelles étapes doivent être effectuées ? Comment estimer l’effort ?
Dans le cadre de cet article, nous aborderons les différentes phases de migration en mettant l’accent sur les concepts de base de la conteneurisation et comment transformer un monolithe en micro-services.
Partir d’un projet existant
Pourquoi moderniser une application ?
Le monolithe représente le modèle traditionnel de conception d’un programme informatique. Mais à mesure que les applications se développent, ce type d’architecture devient contre-productif. Les nouvelles fonctionnalités prennent plus de temps à être délivrées, tout comme le temps de montée en compétences des nouveaux développeurs. Tout cela a un impact fort sur l’application. Une architecture basée sur des micro-services permet de résoudre ces problèmes.
En modernisant l’application d’une architecture monolithique vers une architecture micro-service, l’application peut être divisée en plusieurs fonctions (micro-services) qui nous permettent de travailler indépendamment sur des composants individuels, augmentant ainsi la vitesse de développement logiciel et réduisant le temps nécessaire pour les délivrer.
Définition du micro-service
L’architecture micro-service a été créée pour répondre au besoin de l’agilité. Les entreprises doivent analyser leurs données, innover et lancer de nouveaux produits et services plus rapidement que leurs concurrents. Elles doivent être flexibles pour répondre aux besoins changeants de leurs clients. La migration vers l’architecture de micro-services permet d’atteindre ces objectifs.
Parmi les critères d’adoption des micro-services, on peut citer :
- Croissance de l’entreprise : comme l’application sert plus de clients et traite plus de transactions, elle a besoin de plus de capacité et de ressources ;
- Capacité à absorber les pics de charge sans risquer l’interruption de service ;
- Capacité à faire évoluer/délivrer de nouvelles fonctionnalités applicatives plus rapidement pour répondre aux besoins.
La transformation à l’aide du pattern Domain-Driven-Design
La migration d’un monolithe vers un micro-service nécessite un temps et un investissement importants pour éviter les pannes ou les dysfonctionnements.
Commençons par comprendre l’intérêt des micro-services :
- Les services peuvent évoluer indépendamment en fonction des besoins des utilisateurs ;
- Les services peuvent évoluer indépendamment pour répondre à la demande des utilisateurs ;
- Les cycles de développements s’accélèrent pour délivrer des fonctionnalités plus rapidement ;
- La segmentation en micro-services rend l’application plus tolérante aux pannes et résiliente à la défaillance d’un de ses composants ;
- Les tests deviennent plus consistants grâce au DDD.
La stratégie de migration consiste à réaliser la refactorisation de l’application en la découpant en des services plus petits. On peut synthétiser les grands principes de la refactorisation ainsi :
- Arrêter d’ajouter des fonctionnalités au monolithe ;
- Séparer le Frontend du Backend ;
- Décomposer et découpler le monolithe en une série de micro-services ;
- Isoler les opérations qui peuvent échouer pour améliorer la stabilité et la résilience de l’application.
Pour illustrer notre propos, observons une application simple construite autour de micro-services.
Notre application va authentifier dans un premier temps les utilisateurs en leur permettant de générer des tokens JWT. Par la suite, ces tokens leur serviront à exécuter un ensemble d’actions.
Dans ce qui suit, nous allons détailler les différents composants de notre application :
- La partie frontale est une application JavaScript. Elle fournit une interface utilisateur et a été créée avec VueJS ;
- L’API Auth est écrite en Go et fournit des fonctionnalités d’autorisation. Elle génère des jetons JWT à utiliser avec d’autres APIs ;
- L’API Users est un projet Spring Boot écrit en Java. Elle fournit des profils utilisateur. Cette API permet de retourner un utilisateur ou la liste des utilisateurs ;
- L’API TaskRecords est écrite en NodeJS et fournit des fonctionnalités CRUD sur les enregistrements de tâches de l’utilisateur. En outre, elle enregistre les opérations de création et de suppression dans une Azure Table Storage, afin que les tâches puissent éventuellement être traitées ultérieurement par un log message processor.
Cette architecture est le résultat d’une mise en place du pattern Domain-Driven-Design qui facilite cette décomposition et ce découpage, en passant par une bonne compréhension métier et à l’aide des quelques Cloud-design patterns qui peuvent découler de l’application de cette approche et qui représentent un avantage pour une meilleure migration.
Ces patterns sont :
- Backend for Frontend pattern (BFF) Bulkhead pattern ;
- Retry pattern ;
- Circuit Breaker.
Backend for Frontend pattern
Après avoir migré notre monolithe vers des micro-services, nous devons normaliser la communication entre composants front et back, tout en permettant à la couche Backend de continuer à évoluer. Pour adresser cette problématique, on peut utiliser le pattern Backends For Frontends pour introduire une couche intermédiaire (BFF) qui va formater les données pour le compte du composant Frontend. Le composant Backend peut donc continuer à évoluer de façon indépendante.
Bulkhead pattern
L’utilisation du Cloud pattern Bulkhead va permettre de rendre notre application plus résiliente en compartimentant en sections à la manière de la coque d’un navire. Si la coque d’un navire est compromise, seule la section endommagée se remplit d’eau, ce qui empêche le navire de couler. Appliqué à notre application, ce principe consiste à isoler nos micro-services dans des conteneurs dédiés.
Retry pattern
Certaines erreurs peuvent être transitoires. Avant de les considérer comme de véritables erreurs, il convient de réessayer en ajoutant un délai.
Circuit Breaker pattern
Le pattern Circuit Breaker empêche une application de tenter en permanence une opération avec de fortes chances d’échec. C’est une évolution du pattern précédent. On introduit une bascule vers une seconde chaine applicative vers laquelle on redirige les requêtes en attendant que la chaine applicative principale soit de nouveau opérationnelle.
Migrer votre projet sur le Cloud
Avant de commencer à déployer des micro-services sur Kubernetes, nous allons tout d’abord vous parler de ce qu’est Kubernetes et des raisons pour lesquelles nous devrions l’utiliser pour déployer des micro-services.
Dans cette partie, nous détaillerons les composants Kubernetes et l’architecture générale des opérations. Nous discuterons également des définitions minimales de manifeste pour le déploiement de micro-services, le déploiement des ReplicaSets, des pods et des services.
Composants Kubernetes
Nous allons détailler les composants utilisés par Kubernetes lors du déploiement d’une application. Nous expliquerons les éléments de base que nous pouvons utiliser pour déployer les micro-services sur K8s comme le déploiement, les ReplicaSets, les pods et les services.
Pods
Les pods sont les plus petites unités déployables qu’on peut gérer dans Kubernetes. Un pod est un groupe d’un ou plusieurs conteneurs, avec des ressources de stockage/réseau partagées et une spécification sur la manière d’exécuter les conteneurs.
ReplicaSet
L’objectif d’un ReplicaSet est de maintenir un ensemble stable de pods de réplique en cours d’exécution à tout moment. Par exemple, il est souvent utilisé pour garantir la disponibilité d’un nombre spécifié de pods identiques.
Deployments
Un déploiement fournit des mises à jour déclaratives pour les pods et les ReplicaSets.
On commence par décrire un état souhaité dans un déploiement et le contrôleur de déploiement modifie l’état réel en l’état souhaité. On pourra définir des déploiements pour créer de nouveaux ReplicaSets ou pour supprimer des déploiements existants et adopter toutes leurs ressources avec de nouveaux déploiements.
Service
Ce composant définit une manière abstraite d’exposer une application exécutée sur un ensemble de pods en tant que service réseau. Kubernetes donne aux pods leurs propres adresses IP et un seul nom DNS pour un ensemble de pods, et peut équilibrer la charge entre eux.
ConfigMaps
Un ConfigMap est un objet API utilisé pour stocker des données non confidentielles dans des paires clé-valeur. Les pods peuvent consommer des ConfigMaps en tant que variables d’environnement, arguments de ligne de commande ou fichiers de configuration dans un volume. La bonne pratique est d’externaliser ces configurations dans les ConfigMaps pour rendre le déploiement indépendant de l’environnement.
Cette idée est préconisée par la méthodologie Twelve-Factor App qui consiste à stocker la configuration dans des variables d’environnements. L’avantage de cette approche est que les variables d’environnements sont faciles à changer entre les déploiements sans changer de code, contrairement aux fichiers de configuration qui peuvent être une source de risque s’ils sont répertoriés accidentellement dans notre code source.
Secrets
Les secrets Kubernetes nous permettent de stocker et de gérer des informations sensibles, telles que les mots de passe, les tokens Oauth et les clés SSH. En effet, stocker des informations confidentielles dans un secret est plus sûr et plus flexible que de les mettre en clair dans une définition de pod ou dans une image de conteneur.
Exposition des micro-services dans les clusters Kubernetes
K8s facilite nativement la découverte de services via le service DNS nativement présent. Nos pods sont regroupés en ReplicatSets qui sont exposés via un service Kubernetes. De là, nous sommes en mesure d’exposer cette ressource K8s en interne à l’aide d’un service et en externe à l’aide d’un Ingress pour la rendre accessible de l’extérieur.
Quelques bonnes pratiques de conteneurisation d’applications
Démarrer avec un conteneur le plus léger possible
Pour conteneuriser une application, nous devons tout d’abord choisir une image de base. Nous vous conseillons de bien regarder ce qui est disponible (tags, OS, déclinaisons) et d’utiliser des images de base les plus simples possible, car elles présentent la surface d’attaque la plus faible. Dans ce domaine, la distribution Linux GAne se démarque des autres par sa taille extrêmement réduite mais aussi par le nombre de vulnérabilités qui peuvent y être détectées. En réduisant la surface d’attaque, on améliore la sécurité.
Tout ce qui est inutile doit être expurgé de notre image de conteneur. Le contenu statique (images, scripts, styles CSS…) peut même être externalisé dans un CDN.
Externaliser la configuration dans les variables d’environnement
Dans les fichiers de déploiement applicatif (chart Helm ou fichiers YAML), nous avons la possibilité de renseigner des variables d’environnement pour les conteneurs. On externalise des éléments en respect de la règle config des Twelve Factor App.
Observabilité
Afin de pouvoir monitorer une application conteneurisée et ses dépendances (base de données, services…), il est recommandé d’implémenter des APIs de health checks. Cela se fait au niveau du code source de l’application. Sans entrer dans le détail, il faudra mettre à disposition au moins un endpoint qui permettra de donner une réponse avec l’état de santé des composants qu’on souhaite suivre.
Voici un exemple avec l’état de santé d’une application et de la connexion à SQL Server :
⚠️Si nous souhaitons déployer notre application conteneurisée sur Kubernetes, ce point est très important (cf. liveness, readiness probes) et il est très souvent oublié. Mieux vaut s’y préparer pour éviter les mauvaises surprises.
Rootless container
Pour les conteneurs Linux, il est recommandé de ne pas faire tourner les conteneurs en mode root. Nous devons affecter un utilisateur et lui donner explicitement les permissions nécessaires.
Accélérateurs pour vos projets de migration vers Kubernetes
Sur Kubernetes, beaucoup de projets open-source ou produits éditeurs peuvent répondre aux problématiques autour de l’architecture micro-services. Nous allons nous focaliser sur deux d’entre eux :
- Bridge to Kubernetes ;
- Dapr – Distributed Application Runtime.
Bridge to Kubernetes
Bridge to Kubernetes vient remplacer Azure Dev Spaces. La solution est intégrée à Visual Studio et Visual Studio Code, et permet de substituer un conteneur dans Kubernetes par votre application en local afin de :
- Déboguer l’application sans la conteneuriser ;
- Tester votre projet avec des données de l’environnement cible ;
- Utiliser les services que vous ne souhaitez pas / ne pouvez pas installer en local.
La solution simplifie le développement d’applications micro-services et permet temporairement d’exposer son application locale :
- Pour tous ceux qui accèdent à l’application : démonstration à d’autres membres de l’équipe par exemple ;
- Pour soi (isolation mode) : debug et test.
Dapr – Distributed Application Runtime
Dapr est un runtime permettant de créer des applications/jobs conteneurisés dans le langage que l’on souhaite et déployables sur Kubernetes. Lancé fin 2019, le projet est en GA (Generally Available – disponible publiquement) depuis la mi-février 2021.
Pour le développeur, les principales forces de Dapr sont :
- Les APIs Dapr mises à disposition, permettant d’implémenter des patterns récurrents pour les architectures Cloud et micro-services : utilisation de l’API Binding avec un mécanisme de retry par exemple ;
- Être agnostique des composants externes :
- Si on souhaite se connecter à un Vault pour récupérer un secret, on peut se connecter à Azure Key Vault ou à Hashicorp Vault avec le même code applicatif ;
- Dans un scénario hybride ou multicloud, on peut basculer d’un service on-premise à un service Cloud, sans changer le code source de l’application.
- La possibilité de développer en local (avec Docker), de déployer le même code applicatif et le même type de composant Dapr sur un environnement Kubernetes.
L’essentiel à retenir sur la stratégie de migration sur K8s
Tout au long de cet article, nous avons vu que si nous voulons tirer parti de la plateforme Kubernetes, nous devons transformer nos applications. La migration de chacune d’entre elles devra donc être préparée avec soin pour transformer nos monolithes applicatifs en applications « Cloud native ». C’est certes un investissement conséquent mais nécessaire pour moderniser nos applications. Pour vous guider, aidez-vous des Twelve-Factor App et des différents patterns architecturaux développés tout au long de cet article. Le prochain article va s’attacher à un autre domaine : celui de l’observabilité, que ce soit pour la plateforme Kubernetes mais aussi pour vos applications.
Pour aller plus loin sur Kubernetes
Pour mettre en pratique tout ce que vous avez appris au cours du Mois du Conteneur, Cellenza vous propose de voir le replay du webinaire de clôture. Pendant une heure, Benoît Sautière (Senior Technical Officer chez Cellenza), Stanislas Quastana (Cloud Solution Architect chez Microsoft) et Louis-Guillaume Morand (Cloud Solution Architect chez Microsoft) vous présentent les différentes étapes à suivre pour adopter Kubernetes dans votre entreprise !
Retrouvez également tous les articles du Mois du Conteneur :
- Un mois pour tout savoir sur Kubernetes
- Kubernetes au service de votre stratégie applicative
- Kubernetes : un écosystème à construire
- Stratégie de CI/CD sur Kubernetes
- Mettre en place sa stratégie de migration vers Kubernetes
- Comment intégrer Kubernetes dans sa stratégie de monitoring ?
- Sécuriser son service Kubernetes
Tous ces articles sont compilés dans un livre blanc téléchargeable gratuitement.
Article co-rédigé par Jérôme Thin, Wael Amri et Adnan El Akkaoui