werf - notre outil pour CI/CD dans Kubernetes (aperçu et reportage vidéo)

Le 27 mai dans la salle principale de la conférence DevOpsConf 2019, organisée dans le cadre du festival RIT++ 2019, dans le cadre de la section « Livraison continue », un rapport a été présenté « werf - notre outil pour CI/CD dans Kubernetes ». Il parle de ceux problèmes et défis auxquels tout le monde est confronté lors du déploiement sur Kubernetes, ainsi que sur les nuances qui peuvent ne pas être immédiatement perceptibles. En analysant les solutions possibles, nous montrons comment cela est implémenté dans un outil Open Source cour.

Depuis la présentation, notre utilitaire (anciennement connu sous le nom de dapp) a franchi une étape historique de 1000 étoiles sur GitHub — nous espérons que sa communauté croissante d'utilisateurs facilitera la vie de nombreux ingénieurs DevOps.

werf - notre outil pour CI/CD dans Kubernetes (aperçu et reportage vidéo)

Ainsi, nous présentons vidéo du reportage (~47 minutes, beaucoup plus informatif que l'article) et l'extrait principal sous forme de texte. Aller!

Livraison de code à Kubernetes

On ne parlera plus de werf, mais de CI/CD dans Kubernetes, ce qui implique que nos logiciels sont packagés dans des conteneurs Docker (j'en ai parlé dans rapport 2016), et des K8 seront utilisés pour l'exécuter en production (plus à ce sujet dans 2017 année).

À quoi ressemble la livraison dans Kubernetes ?

  • Il existe un référentiel Git avec le code et les instructions pour le construire. L'application est intégrée à une image Docker et publiée dans le registre Docker.
  • Le même référentiel contient également des instructions sur la façon de déployer et d'exécuter l'application. Au stade du déploiement, ces instructions sont envoyées à Kubernetes, qui reçoit l'image souhaitée du registre et la lance.
  • De plus, il y a généralement des tests. Certaines d'entre elles peuvent être effectuées lors de la publication d'une image. Vous pouvez également (en suivant les mêmes instructions) déployer une copie de l'application (dans un espace de noms K8 distinct ou un cluster distinct) et y exécuter des tests.
  • Enfin, vous avez besoin d'un système CI qui reçoit les événements de Git (ou les clics de boutons) et appelle toutes les étapes désignées : créer, publier, déployer, tester.

werf - notre outil pour CI/CD dans Kubernetes (aperçu et reportage vidéo)

Il y a quelques notes importantes ici :

  1. Parce que nous avons une infrastructure immuable (infrastructure immuable), l'image de l'application utilisée à toutes les étapes (staging, production, etc.), il doit y en avoir un. J'en ai parlé plus en détail et avec des exemples. ici.
  2. Parce que nous suivons l’approche infrastructure as code (IaC), le code de l'application, les instructions pour l'assembler et le lancer doivent être exactement dans un seul référentiel. Pour plus d'informations à ce sujet, voir le même rapport.
  3. Chaîne de livraison (livraison) on le voit habituellement ainsi : l'application a été assemblée, testée, publiée (étape de sortie) et voilà, la livraison a eu lieu. Mais en réalité, l'utilisateur obtient ce que vous avez déployé, aucun puis quand vous l'avez livré à la production, et quand il a pu s'y rendre et que cette production a fonctionné. Donc je crois que la chaîne de livraison se termine seulement au stade opérationnel (Cours), ou plus précisément, même au moment où le code a été retiré de la production (en le remplaçant par un nouveau).

Revenons au schéma de livraison ci-dessus dans Kubernetes : il a été inventé non seulement par nous, mais littéralement par tous ceux qui ont été confrontés à ce problème. En fait, ce modèle s'appelle désormais GitOps (vous pouvez en savoir plus sur le terme et les idées qui le sous-tendent ici). Regardons les étapes du projet.

Étape de construction

Il semblerait que vous puissiez parler de création d'images Docker en 2019, lorsque tout le monde saura écrire des fichiers Docker et exécuter docker build?.. Voici les nuances auxquelles je voudrais prêter attention :

  1. Poids de l'image compte, alors utilisez plusieurs étagesde ne laisser dans l'image que l'application réellement nécessaire à l'opération.
  2. Nombre de couches doit être minimisé en combinant des chaînes de RUN-commandes selon le sens.
  3. Cependant, cela ajoute des problèmes débogage, car lorsque l'assembly plante, vous devez trouver la bonne commande dans la chaîne à l'origine du problème.
  4. Vitesse d'assemblage important car nous voulons déployer rapidement les changements et voir les résultats. Par exemple, vous ne souhaitez pas reconstruire les dépendances dans les bibliothèques de langages à chaque fois que vous créez une application.
  5. Souvent, à partir d'un référentiel Git dont vous avez besoin beaucoup d'images, qui peut être résolu par un ensemble de Dockerfiles (ou d'étapes nommées dans un fichier) et un script Bash avec leur assemblage séquentiel.

Ce n’était que la pointe de l’iceberg auquel tout le monde est confronté. Mais il y a d’autres problèmes, notamment :

  1. Souvent, au stade de l'assemblage, nous avons besoin de quelque chose monter (par exemple, mettez en cache le résultat d'une commande comme apt dans un répertoire tiers).
  2. Nous voulons Ansible au lieu d'écrire en shell.
  3. Nous voulons construire sans Docker (pourquoi avons-nous besoin d'une machine virtuelle supplémentaire dans laquelle nous devons tout configurer pour cela, alors que nous avons déjà un cluster Kubernetes dans lequel nous pouvons exécuter des conteneurs ?).
  4. Assemblage parallèle, qui peut s'entendre de différentes manières : différentes commandes du Dockerfile (si le multi-stage est utilisé), plusieurs commits d'un même référentiel, plusieurs Dockerfiles.
  5. Assemblage distribué: Nous souhaitons collecter des objets dans des pods qui sont « éphémères » car leur cache disparaît, ce qui signifie qu'il doit être stocké quelque part séparément.
  6. Enfin, j'ai nommé le summum des désirs automagie: Il serait idéal d'aller au référentiel, de taper une commande et d'obtenir une image prête à l'emploi, assemblée avec une compréhension de comment et quoi faire correctement. Cependant, personnellement, je ne suis pas sûr que toutes les nuances puissent être prévues de cette façon.

Et voici les projets :

  • moby/kit de construction — un builder de Docker Inc (déjà intégré aux versions actuelles de Docker), qui tente de résoudre tous ces problèmes ;
  • Kaniko — un constructeur de Google qui vous permet de construire sans Docker ;
  • Buildpacks.io — la tentative de CNCF de faire de la magie automatique et, notamment, une solution intéressante avec le rebase des calques ;
  • et un tas d'autres utilitaires, tels que construire, outils authentiques/img...

... et regardez combien d'étoiles ils ont sur GitHub. C'est-à-dire, d'une part, docker build existe et peut faire quelque chose, mais en réalité le problème n'est pas complètement résolu - la preuve en est le développement parallèle de collecteurs alternatifs, dont chacun résout une partie des problèmes.

Assemblage au Werf

Nous devons donc cour (plus tôt célèbre comme Dapp) — Un utilitaire open source de la société Flant, que nous réalisons depuis de nombreuses années. Tout a commencé il y a 5 ans avec des scripts Bash qui optimisaient l'assemblage des Dockerfiles, et depuis 3 ans un développement à part entière a été réalisé dans le cadre d'un projet avec son propre référentiel Git (d'abord en Ruby, puis réécrit to Go, et en même temps renommé). Quels problèmes d’assemblage sont résolus dans Werf ?

werf - notre outil pour CI/CD dans Kubernetes (aperçu et reportage vidéo)

Les problèmes surlignés en bleu ont déjà été implémentés, la construction parallèle a été réalisée au sein du même hôte et les problèmes surlignés en jaune devraient être terminés d'ici la fin de l'été.

Étape de publication dans le registre (publier)

Nous avons composé docker push... - qu'est-ce qui pourrait être difficile dans le téléchargement d'une image dans le registre ? Et puis la question se pose : « Quelle balise dois-je mettre sur l’image ? Cela se produit parce que nous avons Gitflow (ou autre stratégie Git) et Kubernetes, et l'industrie essaie de garantir que ce qui se passe dans Kubernetes suit ce qui se passe dans Git. Après tout, Git est notre seule source de vérité.

Qu'est-ce qu'il y a de si dur là-dedans ? Assurer la reproductibilité: à partir d'un commit dans Git, qui est de nature immuable (immuable), vers une image Docker, qui doit rester la même.

C'est aussi important pour nous déterminer l'origine, car nous voulons comprendre à partir de quel commit l'application exécutée dans Kubernetes a été construite (nous pouvons alors faire des différences et des choses similaires).

Stratégies de marquage

Le premier est simple balise git. Nous avons un registre avec une image étiquetée comme 1.0. Kubernetes a une scène et une production, où cette image est téléchargée. Dans Git, nous effectuons des commits et à un moment donné, nous marquons 2.0. Nous le collectons selon les instructions du référentiel et le plaçons dans le registre avec la balise 2.0. Nous le déployons sur scène et, si tout va bien, en production.

werf - notre outil pour CI/CD dans Kubernetes (aperçu et reportage vidéo)

Le problème avec cette approche est que nous avons d'abord placé la balise, puis seulement nous l'avons testée et déployée. Pourquoi? Premièrement, c'est tout simplement illogique : nous publions une version d'un logiciel que nous n'avons même pas encore testée (on ne peut pas faire autrement, car pour vérifier, il faut mettre une balise). Deuxièmement, ce chemin n'est pas compatible avec Gitflow.

La deuxième option - git commit + balise. La branche master a une balise 1.0; pour cela dans le registre - une image déployée en production. De plus, le cluster Kubernetes dispose de contours de prévisualisation et de préparation. Ensuite, nous suivons Gitflow : dans la branche principale pour le développement (develop) nous créons de nouvelles fonctionnalités, ce qui entraîne un commit avec l'identifiant #c1. Nous le collectons et le publions dans le registre en utilisant cet identifiant (#c1). Avec le même identifiant, nous déployons la prévisualisation. Nous faisons la même chose avec les commits #c2 и #c3.

Lorsque nous avons réalisé qu'il y avait suffisamment de fonctionnalités, nous avons commencé à tout stabiliser. Créer une branche dans Git release_1.1 (sur le socle #c3 de develop). Il n'est pas nécessaire de récupérer cette version, car... cela a été fait à l'étape précédente. Par conséquent, nous pouvons simplement le déployer en staging. Nous corrigeons les bugs dans #c4 et déployer de la même manière la mise en scène. Dans le même temps, le développement est en cours develop, où les modifications sont périodiquement extraites de release_1.1. À un moment donné, nous obtenons un commit compilé et téléchargé sur la scène, ce dont nous sommes satisfaits (#c25).

Ensuite, nous fusionnons (avec avance rapide) la branche release (release_1.1) en maître. Nous mettons une balise avec la nouvelle version sur ce commit (1.1). Mais cette image est déjà collectée dans le registre, donc afin de ne pas la collecter à nouveau, nous ajoutons simplement une deuxième balise à l'image existante (elle a désormais des balises dans le registre #c25 и 1.1). Après cela, nous le déployons en production.

Il existe un inconvénient : une seule image est téléchargée vers la préparation (#c25), et en production c'est un peu différent (1.1), mais nous savons que « physiquement » il s’agit de la même image du registre.

werf - notre outil pour CI/CD dans Kubernetes (aperçu et reportage vidéo)

Le véritable inconvénient est qu'il n'y a pas de support pour les commits de fusion, vous devez faire une avance rapide.

On peut aller plus loin et faire une astuce... Regardons un exemple de Dockerfile simple :

FROM ruby:2.3 as assets
RUN mkdir -p /app
WORKDIR /app
COPY . ./
RUN gem install bundler && bundle install
RUN bundle exec rake assets:precompile
CMD bundle exec puma -C config/puma.rb

FROM nginx:alpine
COPY --from=assets /app/public /usr/share/nginx/www/public

Construisons un fichier à partir de celui-ci selon le principe suivant :

  • SHA256 à partir des identifiants des images utilisées (ruby:2.3 и nginx:alpine), qui sont des sommes de contrôle de leur contenu ;
  • toutes les équipes (RUN, CMD et ainsi de suite.);
  • SHA256 à partir des fichiers ajoutés.

... et prenez la somme de contrôle (encore une fois SHA256) d'un tel fichier. Ce Signature tout ce qui définit le contenu de l'image Docker.

werf - notre outil pour CI/CD dans Kubernetes (aperçu et reportage vidéo)

Revenons au schéma et au lieu de commits, nous utiliserons de telles signatures, c'est à dire. étiquetez les images avec des signatures.

werf - notre outil pour CI/CD dans Kubernetes (aperçu et reportage vidéo)

Désormais, lorsqu'il est nécessaire, par exemple, de fusionner les modifications d'une release vers master, on peut faire un vrai merge commit : il aura un identifiant différent, mais la même signature. Avec le même identifiant, nous déploierons l'image en production.

L'inconvénient est qu'il ne sera désormais plus possible de déterminer quel type de validation a été poussé en production - les sommes de contrôle ne fonctionnent que dans un sens. Ce problème est résolu par une couche supplémentaire avec des métadonnées - je vous en dirai plus plus tard.

Marquage dans Werf

Dans werf, nous sommes allés encore plus loin et nous nous préparons à faire une construction distribuée avec un cache qui n'est pas stocké sur une seule machine... Nous construisons donc deux types d'images Docker, nous les appelons étape и image.

Le référentiel werf Git stocke des instructions spécifiques à la build qui décrivent les différentes étapes de la build (avantInstallation, installer, avant l'installation, installation). Nous collectons l'image de la première étape avec une signature définie comme la somme de contrôle des premières étapes. Ensuite, nous ajoutons le code source, pour la nouvelle image de scène, nous calculons sa somme de contrôle... Ces opérations sont répétées pour toutes les étapes, ce qui nous permet d'obtenir un ensemble d'images de scène. Ensuite, nous créons l'image finale, qui contient également des métadonnées sur son origine. Et nous marquons cette image de différentes manières (détails plus tard).

werf - notre outil pour CI/CD dans Kubernetes (aperçu et reportage vidéo)

Supposons qu'après cela, un nouveau commit apparaisse dans lequel seul le code de l'application a été modifié. Que va-t-il se passer ? Pour les modifications de code, un patch sera créé et une nouvelle image de scène sera préparée. Sa signature sera déterminée comme la somme de contrôle de l'ancienne image de scène et du nouveau patch. Une nouvelle image finale sera formée à partir de cette image. Un comportement similaire se produira avec des changements à d’autres étapes.

Ainsi, les images de scène constituent un cache qui peut être stocké de manière distribuée et les images déjà créées à partir de celui-ci sont téléchargées dans le registre Docker.

werf - notre outil pour CI/CD dans Kubernetes (aperçu et reportage vidéo)

Nettoyer le registre

Nous ne parlons pas de supprimer les couches restées suspendues après la suppression des balises - il s'agit d'une fonctionnalité standard du registre Docker lui-même. Nous parlons d'une situation où beaucoup de balises Docker s'accumulent et nous comprenons que nous n'en avons plus besoin, mais qu'elles prennent de la place (et/ou nous payons pour cela).

Quelles sont les stratégies de nettoyage ?

  1. Tu ne peux rien faire ne nettoie pas. Parfois, il est vraiment plus facile de payer un peu pour un espace supplémentaire que de démêler un énorme enchevêtrement d'étiquettes. Mais cela ne fonctionne que jusqu'à un certain point.
  2. Réinitialisation complète. Si vous supprimez toutes les images et reconstruisez uniquement celles actuelles dans le système CI, un problème peut survenir. Si le conteneur est redémarré en production, une nouvelle image sera chargée pour celui-ci - une image qui n'a encore été testée par personne. Cela tue l’idée d’une infrastructure immuable.
  3. Bleu vert. Un registre a commencé à déborder - nous téléchargeons des images sur un autre. Même problème que dans la méthode précédente : à quel moment peut-on effacer le registre qui a commencé à déborder ?
  4. Par temps. Supprimer toutes les images datant de plus d'un mois ? Mais il y aura certainement un service qui n'a pas été mis à jour depuis un mois...
  5. manuellement déterminer ce qui peut déjà être supprimé.

Il existe deux options vraiment viables : ne pas nettoyer ou une combinaison de bleu-vert + manuellement. Dans ce dernier cas, nous parlons de ce qui suit : lorsque vous comprenez qu'il est temps de nettoyer le registre, vous en créez un nouveau et vous y ajoutez toutes les nouvelles images au cours, par exemple, d'un mois. Et après un mois, voyez quels pods de Kubernetes utilisent toujours l'ancien registre et transférez-les également vers le nouveau registre.

Où en sommes-nous arrivés cour? Nous collectons :

  1. Tête Git : toutes les balises, toutes les branches, en supposant que nous avons besoin de tout ce qui est balisé dans Git dans les images (et sinon, nous devons le supprimer dans Git lui-même) ;
  2. tous les pods actuellement pompés vers Kubernetes ;
  3. les anciens ReplicaSets (ce qui a été récemment publié), et nous prévoyons également d'analyser les versions de Helm et d'y sélectionner les dernières images.

... et créez une liste blanche à partir de cet ensemble - une liste d'images que nous ne supprimerons pas. Nous nettoyons tout le reste, après quoi nous trouvons les images de scène orphelines et les supprimons également.

Étape de déploiement

Déclaration fiable

Le premier point sur lequel je voudrais attirer l'attention dans le déploiement est le déploiement de la configuration des ressources mise à jour, déclarée de manière déclarative. Le document YAML original décrivant les ressources Kubernetes est toujours très différent du résultat réellement exécuté dans le cluster. Parce que Kubernetes ajoute à la configuration :

  1. identifiants;
  2. des informations de service;
  3. de nombreuses valeurs par défaut ;
  4. section avec l'état actuel ;
  5. les modifications apportées dans le cadre du webhook d’admission ;
  6. le résultat du travail de différents contrôleurs (et du planificateur).

Par conséquent, lorsqu'une nouvelle configuration de ressources apparaît (neufs), nous ne pouvons pas simplement prendre et écraser la configuration actuelle « en direct » avec ( le travail). Pour ce faire, nous devrons comparer neufs avec la dernière configuration appliquée (dernière application) et continuez le travail patch reçu.

Cette approche est appelée Fusion à 2 voies. Il est utilisé, par exemple, dans Helm.

Il y a aussi Fusion à 3 voies, qui diffère en ce que :

  • comparant dernière application и neufs, on regarde ce qui a été supprimé ;
  • comparant neufs и le travail, on regarde ce qui a été ajouté ou modifié ;
  • le patch additionné est appliqué à le travail.

Nous déployons plus de 1000 2 applications avec Helm, nous vivons donc réellement avec une fusion bidirectionnelle. Cependant, il présente un certain nombre de problèmes que nous avons résolus avec nos correctifs, qui aident Helm à fonctionner normalement.

Statut réel du déploiement

Une fois que notre système CI a généré une nouvelle configuration pour Kubernetes basée sur le prochain événement, il la transmet pour utilisation (appliquer) à un cluster - en utilisant Helm ou kubectl apply. Ensuite, la fusion N-way déjà décrite se produit, à laquelle l'API Kubernetes répond avec approbation au système CI, et cela à son utilisateur.

werf - notre outil pour CI/CD dans Kubernetes (aperçu et reportage vidéo)

Mais il y a un énorme problème : après tout une application réussie ne signifie pas un déploiement réussi. Si Kubernetes comprend quels changements doivent être appliqués et les applique, nous ne savons toujours pas quel sera le résultat. Par exemple, la mise à jour et le redémarrage des pods dans le frontend peuvent réussir, mais pas dans le backend, et nous obtiendrons différentes versions des images de l'application en cours d'exécution.

Pour tout faire correctement, ce schéma nécessite un lien supplémentaire - un tracker spécial qui recevra les informations d'état de l'API Kubernetes et les transmettra pour une analyse plus approfondie de l'état réel des choses. Nous avons créé une bibliothèque Open Source dans Go - chien cube (voir son annonce ici), qui résout ce problème et est intégré à werf.

Le comportement de ce tracker au niveau werf est configuré à l'aide d'annotations placées sur les déploiements ou les StatefulSets. Annotation principale - fail-mode - comprend les significations suivantes :

  • IgnoreAndContinueDeployProcess — nous ignorons les problèmes de déploiement de ce composant et poursuivons le déploiement ;
  • FailWholeDeployProcessImmediately — une erreur dans ce composant arrête le processus de déploiement ;
  • HopeUntilEndOfDeployProcess — nous espérons que ce composant fonctionnera d'ici la fin du déploiement.

Par exemple, cette combinaison de ressources et de valeurs d'annotation fail-mode:

werf - notre outil pour CI/CD dans Kubernetes (aperçu et reportage vidéo)

Lorsque nous déployons pour la première fois, la base de données (MongoDB) n'est peut-être pas encore prête - les déploiements échoueront. Mais vous pouvez attendre le moment où cela démarre, et le déploiement aura quand même lieu.

Il y a deux autres annotations pour kubedog dans werf :

  • failures-allowed-per-replica — le nombre de chutes autorisées pour chaque réplique ;
  • show-logs-until — régule le moment jusqu'à lequel werf affiche (dans la sortie standard) les journaux de tous les pods déployés. La valeur par défaut est PodIsReady (pour ignorer les messages dont nous ne voulons probablement pas lorsque le trafic commence à arriver sur le pod), mais les valeurs sont également valides : ControllerIsReady и EndOfDeploy.

Qu’attendons-nous d’autre du déploiement ?

En plus des deux points déjà décrits, nous souhaitons :

  • voir journaux - et seulement les nécessaires, et pas tout à la suite ;
  • Piste progrès, car si le travail se bloque « en silence » pendant plusieurs minutes, il est important de comprendre ce qui s'y passe ;
  • avoir restauration automatique en cas de problème (il est donc essentiel de connaître l'état réel du déploiement). Le déploiement doit être atomique : soit il va jusqu'au bout, soit tout revient à son état antérieur.

Les résultats de

Pour nous, en tant qu'entreprise, pour mettre en œuvre toutes les nuances décrites à différentes étapes de livraison (construction, publication, déploiement), un système et un utilitaire CI suffisent cour.

Au lieu d'une conclusion :

werf - notre outil pour CI/CD dans Kubernetes (aperçu et reportage vidéo)

Avec l'aide de werf, nous avons bien progressé dans la résolution d'un grand nombre de problèmes pour les ingénieurs DevOps et nous serions heureux si la communauté au sens large essayait au moins cet utilitaire en action. Il sera plus facile d’obtenir un bon résultat ensemble.

Vidéos et diapositives

Vidéo de la performance (~47 minutes) :

Présentation du rapport :

PS

Autres rapports sur Kubernetes sur notre blog :

Source: habr.com

Ajouter un commentaire