Gestion d'équipes en cours de livraison d'une nouvelle version d'application à Kubernetes



Dans notre pratique, nous sommes souvent confrontés à la tâche d'adapter les applications clientes pour qu'elles s'exécutent sur Kubernetes. Lors de la réalisation de ces travaux, un certain nombre de problèmes typiques se posent. Nous avons récemment couvert l'un d'eux dans l'article Fichiers locaux lors du transfert d'une application vers Kubernetes , et l'autre, déjà connecté aux processus CI / CD, sera décrit dans cet article.

Commandes arbitraires avec Helm et werf


Une application n'est pas seulement une logique métier et des données, mais également un ensemble de commandes arbitraires qui doivent être exécutées pour une mise à jour réussie. Il peut s'agir, par exemple, de migrations de bases de données, de «serveurs» pour la disponibilité de ressources externes, de certains transcodeurs ou décompresseurs, de bureaux d'enregistrement dans la découverte de services externe - vous pouvez effectuer différentes tâches sur différents projets.

Que propose Kubernetes pour résoudre ces problèmes? Kubernetes est bon pour exécuter des conteneurs en tant que pods, la solution standard consiste donc à exécuter une commande à partir d'une image. Pour cela, Kubernetes a une primitive de travail qui vous permet d'exécuter le pod avec des conteneurs d'application et surveille l'achèvement de ce pod.

Helm va un peu plus loin et suggère de lancer Job à différentes étapes du processus de déploiement. Nous parlons de hooks Helm avec lesquels vous pouvez exécuter Job avant ou après la mise à jour des manifestes de ressources. D'après notre expérience, il s'agit d'une excellente fonctionnalité de Helm qui peut être utilisée pour résoudre des tâches de déploiement.

Cependant, il est impossible d'obtenir des informations à jour sur l'état des objets lors du déploiement dans Helm, nous utilisons donc l'utilitaire werf , qui permet de surveiller l'état des ressources pendant le déploiement directement à partir du système CI et, en cas d'échec, de diagnostiquer rapidement une panne.

Il s'est avéré que ces fonctionnalités utiles Helm et werf sont parfois mutuellement exclusives, mais il y a toujours une issue. Examinez comment vous pouvez surveiller l'état des ressources et exécuter des commandes arbitraires sur l'exemple des migrations.

Exécution des migrations avant la publication


La mise à jour du schéma de données fait partie intégrante de la sortie de toute application de base de données. Le déploiement standard pour les applications qui appliquent des migrations en exécutant une commande distincte implique les étapes suivantes:

  1. mise à jour de la base de code;
  2. début de la migration;
  3. basculer le trafic vers la nouvelle version de l'application.

Au sein de Kubernetes, le processus devrait être le même, mais adapté à nos besoins:

  1. lancer un conteneur avec un nouveau code, qui peut contenir un nouvel ensemble de migrations;
  2. démarrer le processus d'application des migrations, après avoir fait cela avant de mettre à jour la version de l'application.

Envisagez l'option lorsque la base de données de l'application est déjà en cours d'exécution et que nous n'avons pas besoin de la déployer dans le cadre de la version qui déploie l'application. Deux crochets conviennent pour appliquer les migrations:

  • pre-install - cela fonctionne sur la première version Helm de l'application après avoir traité tous les modèles, mais avant de créer des ressources dans Kubernetes;
  • pre-upgrade - fonctionne lors de la mise à jour de la version Helm et s'exécute, comme la pre-install , après le traitement des modèles, mais avant de créer des ressources dans Kubernetes.

Exemple de travail utilisant Helm et les deux crochets mentionnés:

 --- apiVersion: batch/v1 kind: Job metadata: name: {{ .Chart.Name }}-apply-migrations annotations: "helm.sh/hook": pre-install,pre-upgrade spec: activeDeadlineSeconds: 60 backoffLimit: 0 template: metadata: name: {{ .Chart.Name }}-apply-migrations spec: imagePullSecrets: - name: {{ required ".Values.registry.secret_name required" .Values.registry.secret_name }} containers: - name: job command: ["/usr/bin/php7.2", "artisan", "migrate", "--force"] {{ tuple "backend" . | include "werf_container_image" | indent 8 }} env: {{ tuple "backend" . | include "werf_container_env" | indent 8 }} - name: DB_HOST value: postgres restartPolicy: Never 

Remarque : le modèle YAML ci-dessus a été créé en tenant compte des spécificités de werf. Pour l'adapter à un Casque "propre", il suffit:

  • remplacer {{ tuple "backend" . | include "werf_container_image" | indent 8 }} {{ tuple "backend" . | include "werf_container_image" | indent 8 }} {{ tuple "backend" . | include "werf_container_image" | indent 8 }} vers l'image de conteneur dont vous avez besoin;
  • supprimez la ligne {{ tuple "backend" . | include "werf_container_env" | indent 8 }} {{ tuple "backend" . | include "werf_container_env" | indent 8 }} {{ tuple "backend" . | include "werf_container_env" | indent 8 }} , qui est spécifié dans la clé env .

Ainsi, ce modèle Helm devra être ajouté au .helm/templates , qui contient déjà le reste des ressources de publication. Lorsque werf deploy --stages-storage :local appelé, tous les modèles seront traités en premier, puis ils seront chargés dans le cluster Kubernetes.

Démarrage des migrations pendant le processus de publication


L'option ci-dessus implique l'utilisation de migrations pour le cas où la base de données est déjà en cours d'exécution. Mais que se passe-t-il si nous devons déployer la revue de branche pour l'application et que la base de données est déployée avec l'application en une seule version?

NB : Vous pouvez rencontrer un problème similaire lors du déploiement dans l'environnement de production si vous utilisez Service avec un point de terminaison qui contient l'adresse IP de la base de données pour se connecter à la base de données.

Dans ce cas, les crochets de pre-install et de pre-upgrade ne nous conviennent pas, car l'application essaiera d'appliquer des migrations à la base de données qui n'existe pas encore . Ainsi, il est nécessaire d'effectuer des migrations après la sortie.

Lorsque vous utilisez Helm, une telle tâche est réalisable, car elle ne surveille pas l' état des applications. Après le chargement des ressources dans Kubernetes, les hooks de publication se déclenchent toujours :

  • post-install - après le chargement de toutes les ressources dans K8 à la première version;
  • post-upgrade - après la mise à jour de toutes les ressources dans K8 lors de la mise à jour de la version.

Cependant, comme nous l'avons mentionné ci-dessus, werf dispose d'un système de suivi des ressources lors de la sortie. Je m'attarderai un peu plus sur ce point:

  • Pour le suivi, werf utilise les capacités de la bibliothèque kubedog , dont nous avons déjà parlé dans le blog.
  • Cette fonctionnalité de werf nous permet de déterminer de manière unique l'état de la publication et d'afficher des informations sur la réussite ou l'échec du déploiement dans l'interface du système CI / CD.
  • Sans recevoir ces informations, on ne peut parler d'aucune automatisation du processus de publication, car la création réussie de ressources dans le cluster Kubernetes n'est qu'une des étapes. Par exemple, l'application peut ne pas démarrer en raison d'une configuration incorrecte ou d'un problème de réseau, mais pour voir cela après la helm upgrade , vous devrez effectuer des étapes supplémentaires.

Revenons maintenant à l'application des migrations sur les post-hooks de Helm. Les problèmes rencontrés:

  • De nombreuses applications avant de lancer d'une manière ou d'une autre vérifient l'état du circuit dans la base de données. Par conséquent, sans nouvelles migrations, l'application peut ne pas démarrer.
  • Étant donné que werf, par défaut, garantit que tous les objets sont à l'état Ready , les hooks de publication ne fonctionneront pas et les migrations échoueront.
  • Les objets de suivi peuvent être désactivés via des annotations supplémentaires, mais il est alors impossible d'obtenir des informations fiables sur les résultats du déploiement.

En conséquence, nous sommes arrivés à ce qui suit:

  • Les travaux sont créés avant les ressources principales, il n'est donc pas nécessaire d'utiliser des crochets Helm pour les migrations .
  • Cependant, un travail avec migrations doit être exécuté sur chaque déploiement. Pour que cela se produise, Job doit avoir un nom unique (aléatoire): dans ce cas, pour Helm, c'est à chaque fois un nouvel objet dans la version, qui sera créé dans Kubernetes.
  • Avec un tel lancement, cela n'a aucun sens de craindre que Job s'accumule avec les migrations, car tous auront des noms uniques et le Job précédent est supprimé avec une nouvelle version.
  • Un travail avec des migrations doit avoir un conteneur init qui vérifie la disponibilité de la base de données - sinon nous obtenons un déploiement abandonné (le travail tombera sur le conteneur init).

La configuration résultante ressemble à ceci:

 --- apiVersion: batch/v1 kind: Job metadata: name: {{ printf "%s-apply-migrations-%s" .Chart.Name (now | date "2006-01-02-15-04-05") }} spec: activeDeadlineSeconds: 60 backoffLimit: 0 template: metadata: name: {{ printf "%s-apply-migrations-%s" .Chart.Name (now | date "2006-01-02-15-04-05") }} spec: imagePullSecrets: - name: {{ required ".Values.registry.secret_name required" .Values.registry.secret_name }} initContainers: - name: wait-db image: alpine:3.6 ommand: ["/bin/sh", "-c", "while ! nc -z postgres 5432; do sleep 1; done;"] containers: - name: job command: ["/usr/bin/php7.2", "artisan", "migrate", "--force"] {{ tuple "backend" . | include "werf_container_image" | indent 8 }} env: {{ tuple "backend" . | include "werf_container_env" | indent 8 }} - name: DB_HOST value: postgres restartPolicy: Never 

NB : à strictement parler, les conteneurs init pour vérifier la disponibilité de la base de données sont de toute façon mieux utilisés.

Un exemple de modèle universel pour toutes les opérations de déploiement


Cependant, les opérations qui doivent être effectuées pendant la version peuvent être plus que le lancement des migrations déjà mentionnées. Vous pouvez contrôler l'ordre d'exécution de Job non seulement via les types de crochets, mais également en attribuant un poids à chacun d'eux - via l'annotation helm.sh/hook-weight . Les crochets sont triés par poids dans l'ordre croissant et, si le poids est le même, par nom de ressource.

Avec un grand nombre de tâches, il est pratique de créer un modèle universel pour les values.yaml et de placer la configuration dans values.yaml . Ce dernier peut ressembler à ceci:

 deploy_jobs: - name: migrate command: '["/usr/bin/php7.2", "artisan", "migrate", "--force"]' activeDeadlineSeconds: 120 when: production: 'pre-install,pre-upgrade' staging: 'pre-install,pre-upgrade' _default: '' - name: cache-clear command: '["/usr/bin/php7.2", "artisan", "responsecache:clear"]' activeDeadlineSeconds: 60 when: _default: 'post-install,post-upgrade' 

... et le modèle lui-même est comme ceci:

 {{- range $index, $job := .Values.deploy_jobs }} --- apiVersion: batch/v1 kind: Job metadata: name: {{ $.Chart.Name }}-{{ $job.name }} annotations: "helm.sh/hook": {{ pluck $.Values.global.env $job.when | first | default $job.when._default }} "helm.sh/hook-weight": "1{{ $index }}" spec: activeDeadlineSeconds: {{ $job.activeDeadlineSeconds }} backoffLimit: 0 template: metadata: name: {{ $.Chart.Name }}-{{ $job.name }} spec: imagePullSecrets: - name: {{ required "$.Values.registry.secret_name required" $.Values.registry.secret_name }} initContainers: - name: wait-db image: alpine:3.6 ommand: ["/bin/sh", "-c", "while ! nc -z postgres 5432; do sleep 1; done;"] containers: - name: job command: {{ $job.command }} {{ tuple "backend" $ | include "werf_container_image" | indent 8 }} env: {{ tuple "backend" $ | include "werf_container_env" | indent 8 }} - name: DB_HOST value: postgres restartPolicy: Never {{- end }} 

Cette approche vous permet d'ajouter rapidement de nouvelles commandes au processus de publication et rend la liste des commandes exécutables plus visuelle.

Conclusion


L'article fournit des exemples de modèles qui vous permettent de décrire les opérations courantes que vous devez effectuer lors du lancement d'une nouvelle version de l'application. Bien qu'ils soient le résultat de l'expérience dans la mise en œuvre de processus CI / CD dans des dizaines de projets, nous n'insistons pas sur le fait qu'il n'y a qu'une seule bonne solution pour toutes les tâches. Si les exemples décrits dans l'article ne couvrent pas les besoins de votre projet, nous serons heureux de voir des situations dans les commentaires qui pourraient aider à compléter ce matériel.

Commentaire des développeurs de werf:
À l'avenir, werf prévoit d'introduire des étapes de déploiement des ressources configurables par l'utilisateur. À l'aide de telles étapes, il sera possible de décrire les deux cas et pas seulement.

PS


Lisez aussi dans notre blog:

Source: https://habr.com/ru/post/fr476320/


All Articles