Une meilleure approche du déploiement continu de Google Cloud
Publié: 2022-03-11Le déploiement continu (CD) est la pratique consistant à déployer automatiquement un nouveau code en production. La plupart des systèmes de déploiement continu valident que le code à déployer est viable en exécutant des tests unitaires et fonctionnels, et si tout semble bon, le déploiement se déroule. Le déploiement lui-même se déroule généralement par étapes afin de pouvoir revenir en arrière si le code ne se comporte pas comme prévu.
Les articles de blog ne manquent pas sur la façon d'implémenter votre propre pipeline de CD à l'aide de divers outils tels que la pile AWS, la pile Google Cloud, le pipeline Bitbucket, etc. Mais je trouve que la plupart d'entre eux ne correspondent pas à mon idée de ce qu'est un bon pipeline de CD. devrait ressembler à : celui qui construit en premier, et teste et déploie uniquement ce fichier construit unique.
Dans cet article, je vais créer un pipeline de déploiement continu piloté par les événements qui se construit d'abord, puis exécute des tests sur notre artefact final de déploiement. Cela rend non seulement nos résultats de test plus fiables, mais rend également le pipeline de CD facilement extensible. Cela ressemblerait à ceci :
- Un commit est fait dans notre référentiel source.
- Cela déclenche une construction de l'image associée.
- Les tests sont exécutés sur l'artefact construit.
- Si tout semble bon, l'image est déployée en production.
Cet article suppose au moins une familiarité passagère avec Kubernetes et la technologie des conteneurs, mais si vous n'êtes pas familier ou avez besoin d'un rappel, consultez Qu'est-ce que Kubernetes ? Guide de conteneurisation et de déploiement.
Le problème avec la plupart des configurations de CD
Voici mon problème avec la plupart des pipelines de CD : ils font généralement tout dans le fichier de construction. La plupart des articles de blog que j'ai lus à ce sujet auront une variante de la séquence suivante dans le fichier de construction dont ils disposent ( cloudbuild.yaml
pour Google Cloud Build, bitbucket-pipeline.yaml
pour Bitbucket).
- Exécuter des tests
- Créer une image
- Pousser l'image vers le dépôt du conteneur
- Mettre à jour l'environnement avec une nouvelle image
Vous n'exécutez pas vos tests sur votre artefact final.
En faisant les choses dans cet ordre, vous exécutez vos tests. S'ils réussissent, vous construisez l'image et continuez avec le reste du pipeline. Que se passe-t-il si le processus de construction modifie votre image de telle manière que les tests ne réussissent plus ? À mon avis, vous devriez commencer par produire un artefact (l'image finale du conteneur) et cet artefact ne devrait pas changer entre la construction et le moment où il est déployé en production. Cela garantit que les données dont vous disposez sur ledit artefact (résultats de test, taille, etc.) sont toujours valides.
Votre environnement de construction possède les "clés du royaume".
En utilisant votre environnement de construction pour déployer votre image sur votre pile de production, vous lui permettez effectivement de modifier votre environnement de production. Je considère cela comme une très mauvaise chose car toute personne ayant un accès en écriture à votre référentiel source peut désormais faire tout ce qu'elle veut dans votre environnement de production.
Vous devez réexécuter l'ensemble du pipeline si la dernière étape a échoué.
Si la dernière étape échoue (par exemple, en raison d'un problème d'informations d'identification), vous devez réexécuter tout votre pipeline, ce qui prend du temps et d'autres ressources qui pourraient être mieux utilisées pour faire autre chose.
Cela m'amène à mon dernier point :
Vos pas ne sont pas indépendants.
De manière plus générale, avoir des étapes indépendantes vous permet d'avoir plus de flexibilité dans votre pipeline. Supposons que vous souhaitiez ajouter des tests fonctionnels à votre pipeline. En ayant vos étapes dans un fichier de build, vous devez faire en sorte que votre environnement de build lance un environnement de test fonctionnel et exécute les tests qu'il contient (très probablement de manière séquentielle). Si vos étapes étaient indépendantes, vous pourriez faire démarrer à la fois vos tests unitaires et vos tests fonctionnels par l'événement "image build". Ils fonctionneraient alors en parallèle dans leur propre environnement.
Ma configuration de CD idéale
À mon avis, une meilleure façon d'aborder ce problème serait d'avoir une série d'étapes indépendantes toutes liées entre elles par un mécanisme d'événement.
Cela présente plusieurs avantages par rapport à la méthode précédente :
Vous pouvez effectuer plusieurs actions indépendantes sur différents événements.
Comme indiqué ci-dessus, la construction réussie d'une nouvelle image publierait simplement un événement "construction réussie". À leur tour, nous pouvons exécuter plusieurs choses lorsque cet événement est déclenché. Dans notre cas, nous commencerions les tests unitaires et fonctionnels. Vous pouvez également penser à des choses comme alerter le développeur lorsqu'un événement d'échec de construction est déclenché ou si les tests échouent.
Chaque environnement a son propre ensemble de droits.
En faisant en sorte que chaque étape se produise dans son propre environnement, nous éliminons le besoin qu'un seul environnement ait tous les droits. Désormais, l'environnement de génération ne peut que générer, l'environnement de test ne peut que tester et l'environnement de déploiement ne peut que déployer. Cela vous permet d'être sûr qu'une fois votre image construite, elle ne changera pas. L'artefact qui a été produit est celui qui se retrouvera dans votre pile de production. Cela permet également de vérifier plus facilement quelle étape de votre pipeline fait quoi, car vous pouvez lier un ensemble d'informations d'identification à une étape.
Il y a plus de flexibilité.
Vous voulez envoyer un e-mail à quelqu'un pour chaque build réussi ? Ajoutez simplement quelque chose qui réagit à cet événement et envoie un e-mail. C'est simple : vous n'avez pas besoin de modifier votre code de build et vous n'avez pas besoin de coder en dur l'e-mail de quelqu'un dans votre référentiel source.
Les tentatives sont plus faciles.
Avoir des étapes indépendantes signifie également que vous n'avez pas à redémarrer l'ensemble du pipeline si une étape échoue. Si la condition d'échec est temporaire ou a été corrigée manuellement, vous pouvez simplement réessayer l'étape qui a échoué. Cela permet un pipeline plus efficace. Lorsqu'une étape de génération prend plusieurs minutes, il est bon de ne pas avoir à reconstruire l'image simplement parce que vous avez oublié de donner à votre environnement de déploiement un accès en écriture à votre cluster.
Mettre en œuvre le déploiement continu de Google Cloud
Google Cloud Platform dispose de tous les outils nécessaires pour créer un tel système en peu de temps et avec très peu de code.
Notre application de test est une simple application Flask qui ne sert qu'un morceau de texte statique. Cette application est déployée sur un cluster Kubernetes qui la dessert sur Internet au sens large.
Je vais implémenter une version simplifiée du pipeline que j'ai présenté plus tôt. J'ai essentiellement supprimé les étapes de test, donc cela ressemble maintenant à ceci:
- Un nouveau commit est effectué sur le référentiel source
- Cela déclenche une création d'image. En cas de succès, il est transmis au référentiel de conteneurs et un événement est publié dans un sujet Pub/Sub
- Un petit script est abonné à ce sujet et vérifie les paramètres de l'image. S'ils correspondent à ce que nous avons demandé, il est déployé sur le cluster Kubernetes.
Voici une représentation graphique de notre pipeline.
Le flux est le suivant :
- Quelqu'un s'engage dans notre référentiel.
- Cela déclenche une génération cloud qui crée une image Docker basée sur le référentiel source.
- La version cloud envoie l'image au référentiel de conteneurs et publie un message sur cloud pub/sub.
- Cela déclenche une fonction cloud qui vérifie les paramètres du message publié (statut du build, nom de l'image construite, etc.).
- Si les paramètres sont bons, la fonction cloud met à jour un déploiement Kubernetes avec la nouvelle image.
- Kubernetes déploie de nouveaux conteneurs avec la nouvelle image.
Code source
Notre code source est une application Flask très simple qui ne sert que du texte statique. Voici la structure de notre projet :

├── docker │ ├── Dockerfile │ └── uwsgi.ini ├── k8s │ ├── deployment.yaml │ └── service.yaml ├── LICENSE ├── Pipfile ├── Pipfile.lock └── src └── main.py
Le répertoire Docker contient tout le nécessaire pour créer l'image Docker. L'image est basée sur l'image uWSGI et Nginx et installe simplement les dépendances et copie l'application dans le bon chemin.
Le répertoire k8s contient la configuration Kubernetes. Il se compose d'un service et d'un déploiement. Le déploiement démarre un conteneur basé sur l'image créée à partir de Dockerfile . Le service démarre ensuite un équilibreur de charge qui a une adresse IP publique et redirige vers le ou les conteneurs d'application.
Construction en nuage
La configuration de la construction cloud elle-même peut être effectuée via la console cloud ou la ligne de commande Google Cloud. J'ai choisi d'utiliser la console cloud.
Ici, nous construisons une image pour n'importe quel commit sur n'importe quelle branche, mais vous pourriez avoir des images différentes pour le développement et la production, par exemple.
Si la compilation réussit, la compilation cloud publiera elle-même l'image dans le registre de conteneurs. Il publiera ensuite un message dans le sujet pub/sub de cloud-builds.
La version cloud publie également des messages lorsqu'une version est en cours et lorsqu'elle échoue, de sorte que vous pouvez également faire réagir les choses à ces messages.
La documentation pour les notifications pub/sub du build cloud est ici et le format du message peut être trouvé ici
Cloud Pub/Sub
Si vous regardez dans votre onglet pub/sub cloud dans la console cloud, vous verrez que la build cloud a créé une rubrique appelée builds cloud. C'est là que la version cloud publie ses mises à jour de statut.
Fonction cloud
Ce que nous allons faire maintenant est de créer une fonction cloud qui se déclenche sur tout message publié dans le sujet cloud-builds. Encore une fois, vous pouvez utiliser la console cloud ou l'utilitaire de ligne de commande Google Cloud. Ce que j'ai fait dans mon cas, c'est que j'utilise la version cloud pour déployer la fonction cloud chaque fois qu'elle est modifiée.
Le code source de la fonction cloud est ici.
Regardons d'abord le code qui déploie cette fonction cloud :
steps: - name: 'gcr.io/cloud-builders/gcloud' id: 'test' args: ['functions', 'deploy', 'new-image-trigger', '--runtime=python37', '--trigger-topic=cloud-builds', '--entry-point=onNewImage', '--region=us-east1', '--source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME']
Ici, nous utilisons l'image Google Cloud Docker. Cela permet d'exécuter facilement les commandes GCcloud. Ce que nous exécutons équivaut à exécuter directement la commande suivante depuis un terminal :
gcloud functions deploy new-image-trigger --runtime=python37 --trigger-topic=cloud-builds --entry-point=onNewImage --region=us-east1 --source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME
Nous demandons à Google Cloud de déployer une nouvelle fonction cloud (ou de la remplacer si une fonction portant ce nom dans cette région existe déjà) qui utilisera l'environnement d'exécution Python 3.7 et sera déclenchée par de nouveaux messages dans la rubrique cloud-builds. Nous indiquons également à Google où trouver le code source de cette fonction (ici, PROJECT_ID et REPO_NAME sont des variables d'environnement qui sont définies par le processus de construction). Nous lui disons également quelle fonction appeler comme point d'entrée.
En passant, pour que cela fonctionne, vous devez donner à votre compte de service cloudbuild à la fois le "développeur de fonctions cloud" et "l'utilisateur du compte de service" afin qu'il puisse déployer la fonction cloud.
Voici quelques extraits commentés du code de la fonction cloud
Les données du point d'entrée contiendront le message reçu sur le sujet pub/sub.
def onNewImage(data, context):
La première étape consiste à obtenir les variables pour ce déploiement spécifique à partir de l'environnement (nous les avons définies en modifiant la fonction cloud dans la console cloud.
project = os.environ.get('PROJECT') zone = os.environ.get('ZONE') cluster = os.environ.get('CLUSTER') deployment = os.environ.get('DEPLOYMENT') deploy_image = os.environ.get('IMAGE') target_container = os.environ.get('CONTAINER')
Nous allons sauter la partie où nous vérifions que la structure du message est ce que nous attendons et nous validons que la construction a réussi et a produit un artefact d'image.
L'étape suivante consiste à s'assurer que l'image qui a été construite est celle que nous voulons déployer.
image = decoded_data['results']['images'][0]['name'] image_basename = image.split('/')[-1].split(':')[0] if image_basename != deploy_image: logging.error(f'{image_basename} is different from {deploy_image}') return
Maintenant, nous obtenons un client Kubernetes et récupérons le déploiement que nous voulons modifier
v1 = get_kube_client(project, zone, cluster) dep = v1.read_namespaced_deployment(deployment, 'default') if dep is None: logging.error(f'There was no deployment named {deployment}') return
Enfin, nous corrigeons le déploiement avec la nouvelle image ; Kubernetes se chargera de le déployer.
for i, container in enumerate(dep.spec.template.spec.containers): if container.name == target_container: dep.spec.template.spec.containers[i].image = image logging.info(f'Updating to {image}') v1.patch_namespaced_deployment(deployment, 'default', dep)
Conclusion
Ceci est un exemple très basique de la façon dont j'aime que les choses soient architecturées dans un pipeline de CD. Vous pourriez avoir plus d'étapes simplement en changeant quel événement pub/sub déclenche quoi.
Par exemple, vous pouvez exécuter un conteneur qui exécute les tests à l'intérieur de l'image et publie un événement en cas de succès et un autre en cas d'échec et réagir à ceux-ci en mettant à jour un déploiement ou en alertant en fonction du résultat.
Le pipeline que nous avons construit est assez simple, mais vous pouvez écrire d'autres fonctions cloud pour d'autres parties (par exemple, une fonction cloud qui enverrait un e-mail au développeur qui a validé le code qui a cassé vos tests unitaires).
Comme vous pouvez le voir, notre environnement de construction ne peut rien changer dans notre cluster Kubernetes, et notre code de déploiement (la fonction cloud) ne peut pas modifier l'image qui a été construite. Notre séparation des privilèges semble bonne, et nous pouvons dormir sur nos deux oreilles en sachant qu'un développeur malhonnête ne fera pas tomber notre cluster de production. Nous pouvons également donner à nos développeurs plus orientés opérations l'accès au code de la fonction cloud afin qu'ils puissent le réparer ou l'améliorer.
Si vous avez des questions, des remarques ou des améliorations, n'hésitez pas à nous contacter dans les commentaires ci-dessous.