Cette série est également disponible en anglais.
Read in English
Chapitre 8
Home LabTutorialsDevOps & Infrastructure

Argo pour Kubernetes : d'Argo CD à Workflows et Image Updater | Un guide complet

24 min de lecture
Argo pour Kubernetes : d'Argo CD à Workflows et Image Updater | Un guide complet
Apprenez à construire un écosystème Argo de bout en bout entièrement natif sur Kubernetes. Ce guide étape par étape couvre la configuration d'Argo CD, Argo Workflows et Argo Image Updater.

J'ai récemment migré tous mes services de GitHub Actions vers un écosystème Argo complet. Cela inclut l'utilisation d'Argo CD pour le déploiement des manifestes Kubernetes, d'Argo Workflows pour la construction des images, et d'Argo Image Updater pour le déploiement continu. Dans ce guide, je vais vous accompagner pas à pas dans la mise en place du même workflow.

Attention : Dans cet article, nous allons apprendre les bases d'Argo et prendrons donc quelques raccourcis par souci de simplicité. Ainsi, certains choix de conception pourraient ne pas être appropriés pour une installation en production.

Vous pouvez suivre ce guide de manière pratique avec le code source complet et les manifestes dans le repos : Argo-tutorial

Écosystème Argo vs GitHub Actions

La première question qui se posent: Pourquoi utiliser Argo au lieu de Github Actions?

Sécurité & Architecture Étant une solution basée sur le modèle pull, Argo est intrinsèquement plus sécurisé. GitHub Actions nécessite un accès externe à votre cluster pour déployer les manifestes. À l'inverse, Argo réside à l'intérieur de votre cluster. Il n'a besoin que de lire votre repos Git ; les identifiants du cluster ne quittent jamais votre réseau.

Nettoyage (Pruning) natif Le pruning (nettoyage) est natif à Argo. Sans cela, supprimer un fichier YAML de votre repos Git vous obligerait à exécuter manuellement kubectl delete pour retirer la même ressource du cluster. A l'inverse, Argo surveille le repos git, ce qui signifie que si un manifeste de ressource est retiré de Git, Argo va automatiquement le retirer du cluster (on peux désactiver cette option). Avec Argo, votre repos est la source de vérité, et votre cluster garde le contrôle.

Visibilité Je trouve également Argo plus facile à déboguer et à visualiser. Son interface web (UI) montre clairement ce qui est déployé et ce qui a mal tourné.

Une capture d'écran montrant le tableau de bord d'Argo CD affichant le statut d'une application nommée 'homepage'.

Indépendance vis-à-vis de GitHub Pour moi, ces fonctionnalités n'étaient pas les principales raisons de ce changement. La curiosité a joué un rôle, mais le principal déclencheur a été que mon organisation GitHub à été signalé comme frauduleux par erreur pendant plusieurs mois. Cela à désactivé plusieurs composants comme les Github Actions.

Pourquoi ? L'une de mes Actions consistait à vérifier tous les liens de mon blog. Elle cherchait les liens morts et effectuait une analyse interne. L'un de ces liens externes avait changé de propriétaire pour quelqu'un sur la liste noire de GitHub. Un workflow qui fonctionnait depuis des mois m'a soudainement valu un signalement, sans préavis, sans e-mail, ni avertissement.

Après des mois de discussions avec le support de GitHub qui refusait simplement d'expliquer le problème, j'ai finalement trouver la solution et récupéré mon compte. Entre-temps, j'avais besoin d'une alternative qui ne dépende pas des caprices d'une organisation externe. J'ai donc tout basculé sur Argo, en ne gardant que mon code sur GitHub (pour l'instant).

La beauté d'Argo, c'est que je pourrais passer à GitLab ou à un Gitea auto-hébergé sans pratiquement aucun changement ; Argo se fiche de l'endroit où les repos sont hébergés. Je suis maintenant totalement indépendant de GitHub. La seule raison pour laquelle je continue d'utiliser GitHub est de m'assurer que mon code est sauvegardé en dehors de mon cluster.

Prérequis

Evidemment, avant de commencer, vous aurez besoin d'un cluster Kubernetes fonctionnel et l'outil CLI kubectl configurée pour y accéder. Lisez cet article et celui-ci si vous voulez en savoir plus à ce sujet.

Vous aurez également besoin de Helm. Helm va nous aider à installer Argo. L'installer est aussi simple que d'exécuter l'une de ces commandes :

#Linux
sudo apt-get install helm
 
#Windows
choco install kubernetes-helm
 
#Mac OS
brew install helm

For more package manager see the official documentation.

Étape 1 : Installer Argo CD

Argo CD est le personnage principal de cette histoire. Son rôle est de surveiller votre repos Git et de maintenir votre cluster Kubernetes synchronisé avec les manifestes qui y sont stockés. Cela signifie que chaque fois que vous commitez une modification dans un manifeste, Argo le verra et l'appliquera directement.

Pour être clair, Argo CD ne gère que vos fichiers yaml, c'est tout. Il ne construit ni ne deploie d'images. Pour cela, nous utiliserons Argo Workflow et Argo Image Updater. Nous verrons comment faire cela plus tard, mais une chose à la fois.

Installer Argo CD avec des valeurs personnalisées

Pour déployer Argo, nous devons d'abord ajouter le repos Helm d'Argo :

helm repo add argo https://argoproj.github.io/argo-helm
helm repo update

Ensuite, il est de bonne pratique de créer un fichier values.yaml pour stocker les paramètres que nous utiliserons pour configurer Argo. Voici un exemple :

global:
  domain: argo.home.arpa
 
configs:
  params:
    server.insecure: true
    server.redirect.https: "false"
 
server:
  ingress:
    enabled: true
    hostname: argo.home.arpa
    annotations:
      traefik.ingress.kubernetes.io/router.entrypoints: web
    paths:
      - /
    pathType: Prefix

Cette configuration indique que l'interface web (UI) d'Argo sera déployée sur argo.home.arpa de manière non sécurisée, ce qui est acceptable pour une configuration locale. Bien sûr, vous pouvez choisir n'importe quelle autre URL pour le hostname, c'est à vous de décider.

Si vous souhaitez un accès à distance avec HTTPS, je vous suggère de lire mon article précédent sur le sujet. Notez que cela est risqué car cela ajoute un point d'attaque potentielle.

Vous pouvez enregistrer ce manifeste où vous le souhaitez. Pour ma part, comme il s'agit d'une infrastructure essentielle, je l'enregistre dans mes repos d'infrastructure, dans un dossier nommé argocd/. Je ferai de même pour le reste des fichiers nécessaires à la configuration d'Argo.

Ensuite, vous pouvez installer Argo via Helm avec cette commande, qui créera le namespace argocd et déploiera Argo CD :

helm upgrade --install argocd argo/argo-cd --namespace argocd --create-namespace -f values.yaml

Une fois terminé, vous devriez pouvoir vous connecter via l'URL configurée, pour moi argo.home.arpa. Pour votre première connexion, votre nom d'utilisateur est "admin" et votre mot de passe est un secret Kubernetes nommé argocd-initial-admin-secret. Vous pouvez le récupérer en ligne de commande comme ceci :

#Linux
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
 
#Windows
$encoded = kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}"
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded))

Vous pouvez également envisager d'utiliser une interface web comme Portainer pour accéder plus facilement aux secrets.

Vous devriez maintenant pouvoir vous connecter et trouver une interface Argo vide, prête à l'emploi. C'est là que vous trouverez vos applications et leur statut. C'est très utile pour le débogage.

Le tableau de bord des Applications Argo CD affichant une grille de cartes d'applications déployées montrant leur état de santé et de synchronisation.

Configurer les identifiants du repos

Argo CD a besoin d'un PAT (Personal Access Token) GitHub pour obtenir un accès en lecture à vos repos Git. Afin de le stocker de manière sécurisée dans votre cluster, créons un secret via un manifeste :

apiVersion: v1
kind: Secret
metadata:
  name: github-org-pat-creds
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repo-creds
type: Opaque
stringData:
  url: https://github.com/Local-pie/
  username: JudicaelPoumay
  password: ghp_xxxxxxxxxxxx

Notez le label secret-type. Argo recherche les secrets avec ce label dans son namespace. Vous pouvez définir plusieurs secrets de ce type si vous avez besoin d'accéder à plusieurs repos ou organisations. Pour ma part, chaque repos lié à mon cluster se trouve dans une seule organisation; ce qui simplifie les choses car je n'ai pas à créer un secret pour chaque nouveau repos, mais c'est à vous de voir.

N'oubliez pas de remplacer ghp_xxxxxxxxxxxx par votre PAT. Vous pouvez en générer un en allant dans GitHub Settings > Developer Settings > Personal Access Tokens (Documentation GitHub). Pour une configuration simple, créez un jeton classique avec les accès en lecture appropriés.

Pour un déploiement en production, préférez un jeton à granularité fine (fine-grained token) avec des permissions de lecture limitées au repos plutôt qu'un PAT classique.

Ensuite, appliquez-le :

kubectl apply -f secret.yaml

Conseil pour les débutants : Ne commitez jamais de secrets, même sur un repos GitHub privé ; les fuites de données arrivent. Si vous souhaitez stocker des secrets dans GitHub tout en restant indépendant de leurs services, je vous suggère d'utiliser des solution comme KubeSeal.

Comment utiliser Argo CD

C'est tout pour l'installation; voyons maintenant comment tout cela fonctionne.

Une application simple

Argo CD garantit que l'état de votre cluster reflète strictement votre repos Git. Ainsi, vous définissez l'état souhaité dans Git à travers un ensemble de fichiers yaml, et Argo CD réconcilie continuellement l'état réel du cluster pour qu'il corresponde.

Pour ce faire, nous devons définir un fichier YAML séparé pour Argo. Ensuite, on l'applique comme n'importe quel autre YAML au cluster Kubernetes. Par exemple, voici la définition de mon application Argo pour mon homepage :

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: homepage
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/Local-pie/homepage.git
    targetRevision: main
    path: k8s
  destination:
    server: "https://kubernetes.default.svc"
    namespace: utils-dev
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

La plupart des champs sont explicites, alors concentrons-nous sur quelques parties intéressantes :

  • project: Les projets sont comme les organisations GitHub, ils fournissent un regroupement logique, des contrôles de sécurité et plus encore. Nous utiliserons default pour nous simplifier la vie.
  • targetRevision: C'est la branche qu'Argo va récupérer et utiliser comme source de vérité.
  • server: Pointe vers votre cluster. Si Argo est auto-hébergé sur le même cluster, c'est https://kubernetes.default.svc.
  • namespaces: Notez que le manifeste de l'Application a un namespace, le même qu'Argo. Mais l'application homepage elle-même a son propre namespace qui est différent.
  • syncPolicy: Définit comment nous voulons synchroniser.
    • Prune: Si un manifeste est retiré de votre repos Git, Argo CD le nettoie immédiatement du cluster.
    • selfHeal: Argo corrige toute dérive de configuration, ce qui signifie que si vous supprimez manuellement quelque chose du cluster, Argo le redéploiera immédiatement.
    • CreateNamespace: Signifie qu'Argo est autorisé à créer le namespace de l'application s'il n'existe pas.

L'argument path est particulièrement intéressant car il contient le chemin vers le dossier de vos manifestes Kubernetes. Il peut s'agir d'un dossier avec un simple ensemble de fichiers YAML, mais aussi d'un dossier avec une configuration de type Helm, avec un fichier values.yaml, Chart.yaml et un dossier templates/. Argo détectera automatiquement lequel vous utilisez.

Note : Le chemin est relatif à la racine Git, et non à l'emplacement de ce fichier d'Application Argo. Personnellement, j'aime conserver la définition de mon Application Argo dans un dossier Argo/ séparé à la racine.

Voici a quoi peut ressembler votre structure de fichiers :

# Configuration Kubernetes simple
Argo/
└── app.yaml
k8s/
├── namespace.yaml
├── deployment.yaml
├── service.yaml
└── ingress.yaml

# Configuration Helm Kubernetes
Argo/
└── app.yaml
k8s/
├── Chart.yaml
├── values.yaml
└── templates/
    ├── namespace.yaml
    ├── ingress.yaml
    ├── deployment.yaml
    └── service.yaml

Une fois configuré, appliquez le fichier Argo avec kubectl apply comme tout le reste jusqu'à présent. Il devrait alors apparaître dans votre interface Argo, et vous devriez voir Argo lire votre repos Git et déployer les manifestes présent. (N'oubliez pas de les commiter avant ;) )

Pour des projets basiques, on peux s'arreter la. Si tout ce que vous avez besoin c'est de deployer des manifests, c'est peut-être sufisant. Evidemment, on peux faire plus avec Argo.

Déployer sur plusieurs environnements

À un moment donné, vous aurez probablement besoin de mettre en place un environnement de pré-production séparé, distinct de la production réelle. Avec Argo, il vous suffit de créer un nouveau fichier d'Application.

Soit vous le faites pointer vers un dossier séparé, soit, si vous utilisez Helm, vous pouvez indiquer à Argo quel values.yaml utiliser et dans quel ordre avec les arguments Helm. Par exemple, voici un configuration pre-prod :

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: homepage
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/Local-pie/homepage.git
    targetRevision: develop
    path: k8s
    helm:
      valueFiles:
        - values.yaml
        - values-preprod.yaml
  destination:
    server: "https://kubernetes.default.svc"
    namespace: utils-dev
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Le dossier k8s ressemblerait alors à ceci :

k8s/
├── Chart.yaml
├── values.yaml
├── values-prd.yaml
├── values-preprod.yaml
└── templates/
    ├── namespace.yaml
    ├── ingress.yaml
    ├── deployment.yaml
    └── service.yaml

Étape 2 : Installer Argo Workflows

Passer à Argo CD est étrange car ce n'est pas vraiment un remplacement pour GitHub Actions tel quel. Mettre à jour des fichiers YAML, c'est bien, mais qu'en est-il si nous avons besoin de workflows pour construire des images ou faire autre chose ? La solution est Argo Workflows !

Argo Workflows est essentiellement un orchestrateur capable de créer et de gérer des Jobs Kubernetes pour faire tout ce que vous voulez. Dans notre cas, nous allons construire et déployer des images à partir de repos GitHub.

Installer Argo Workflows

Commençons par créer un fichier values.yaml.

server:
  # Disables HTTPS inside the pod
  secure: false
 
  # Bypasses the UI login
  extraArgs:
    - --auth-mode=server
 
  # Configures the Ingress
  ingress:
    enabled: true
    ingressClassName: "traefik"
    hosts:
      - workflow.home.arpa
    paths:
      - /
    pathType: Prefix

Notez que, similairement à Argo CD, je désactive également HTTPS car je n'ai ni le besoin ni l'envie de gérer des certificats ici. Dans ce YAML, je définis également l'Ingress pour l'interface web.

kubectl create namespace argo
helm install argo argo/argo-workflows -n argo --create-namespace -f values.yaml

Pourquoi créer un nouvel namespace et ne pas utiliser celui d'Argo CD ? Les séparer prévient les risques de sécurité en empêchant les workflows d'accéder aux secrets de déploiement d'Argo CD. Cela vous permet également de mettre à jour, gérer ou supprimer chaque outil indépendamment sans affecter l'autre.

Une fois terminé, vous devriez pouvoir accéder à l'interface en une minute ou deux. Dans ce cas, aucun mot de passe n'est nécessaire car, dans cet exemple, j'ai décidé de définir --auth-mode=server, ce qui désactive l'identification. Cela peut être acceptable tant que vous avez un réseau local sécurisé et que vous savez ce que vous faites. C'est à vous de décider.

Capture d'écran de l'interface web d'Argo Workflows affichant une liste de cron workflows configurés.

Configurer un namespace séparé pour les Workflows

Vous ne voulez pas que vos workflows se trouvent dans le même namespace que le contrôleur pour deux raisons.

  1. Sécurité : Des espaces de noms séparés signifient un rayon d'impact plus petit en cas de problème.
  2. Ressources : Les workflows peuvent être gourmands en ressources et pourraient priver le contrôleur de ressources. Il est plus facile de définir des quotas de ressources dans un namespace séparé.

Créons donc un troisième namespace pour les workflows :

kubectl create namespace argo-builds

Lorsque nous créerons des workflows plus tard, nous les assignerons à cet namespace.

Configurer les identifiants du repos

Argo Workflows a également besoin d'un PAT GitHub pour lire vos repos Git. Similairement à Argo CD, créons un secret via un manifeste :

apiVersion: v1
kind: Secret
metadata:
  name: github-org-pat-creds
  namespace: argo-builds
type: Opaque
stringData:
  url: https://github.com/Local-pie/
  username: JudicaelPoumay
  password: ghp_xxxxxxxxxxxx

Notez qu'il n'y a pas de label ici, c'est parce que nous utiliserons ce secret explicitement plus tard. Argo Workflow ne le cherchera pas. Notez aussi que le namespace est celui où les workflows s'exécuteront.

N'oubliez pas de l'enregistrer de manière sécurisée et de l'appliquer.

Configurer le RBAC pour les workflows

Nos workflows auront besoin de permissions pour gérer les résultats des tâches et les déploiements. Voici un autre manifeste YAML à appliquer pour définir l'ensemble des permissions nécessaires :

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: workflow-taskresult-manager
  namespace: argo-builds
rules:
  - apiGroups: ["argoproj.io"]
    resources: ["workflowtaskresults", "workflows", "cronworkflows"]
    verbs: ["create", "get", "list", "watch", "patch", "update"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "patch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: workflow-taskresult-binding
  namespace: argo-builds
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: workflow-taskresult-manager
subjects:
  - kind: ServiceAccount
    name: default
    namespace: argo-builds

Définir les quotas de ressources

Enfin, les workflows peuvent devenir très lourds, surtout sur de petits clusters. Vous pouvez limiter les ressources CPU pour les workflows dans le namespace argo-builds pour éviter l'épuisement des ressources. Par exemple, voici un manifeste ResourceQuota qui plafonne le CPU à 2 cœurs et les jobs concurrents à 5 :

apiVersion: v1
kind: ResourceQuota
metadata:
  name: job-and-cpu-quota
  namespace: argo-builds
spec:
  hard:
    requests.cpu: "1"
    limits.cpu: "2"
    count/jobs.batch: "5"

Soyez particulièrement prudent lorsque vous limitez les jobs concurrents. Les jobs terminés comptent toujours dans la limite jusqu'à ce que Kubernetes les supprime. Cela peut empêcher de nouveaux jobs de s'exécuter jusqu'à ce que les anciens soient nettoyés. Kubernetes n'est pas très flexible sur ce point, et il n'y a pas de moyen précis de distinguer les jobs actifs des jobs terminés dans la configuration de la limite.

Comment configurer un Argo Workflow

Un Workflow de base

Commençons par les bases : un simple Argo Workflow pour afficher "Hello World" en deux étapes.

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: two-stage-
  namespace: argo-builds
spec:
  entrypoint: main
 
  templates:
  # The main entrypoint now defines a sequence of steps
  - name: main
    steps:
    - - name: stage-one
        template: echo-message
        arguments:
          parameters:
          - name: message
            value: "Starting stage one... Hello!"
 
    - - name: stage-two
        template: echo-message
        arguments:
          parameters:
          - name: message
            value: "Stage one complete. Hello from stage two!"
 
  # A reusable template that accepts a parameter
  - name: echo-message
    inputs:
      parameters:
      - name: message
    container:
      image: alpine:latest
      command: ["sh", "-c"]
      args: ["echo '{{inputs.parameters.message}}'"]

Voici ce qu'il se passe :

  • generateName: Définit le préfixe du workflow. Argo ajoute un suffixe aléatoire lors de l'instanciation, permettant plusieurs instances sans conflit de nom. Si vous utilisez un name fixe à la place, vous ne pouvez pas le ré-exécuter tant que le précédent n'est pas nettoyé.
  • namespace: Comme mentionné, nous utilisons le namespace dédié argo-builds pour nos jobs.
  • templates: Comme des fonctions dans un langage de programmation, ce sont des blocs de code réutilisables.
    • echo-message: Utilise l'image Alpine pour afficher un message paramétré.
    • stage-one & stage-two: Réutilisent echo-message pour afficher des textes différents.
  • entrypoint: Spécifie le template principal à exécuter (ici, main).
  • steps: Définit le workflow comme une séquence d'étapes. Dans ce cas, nos deux étapes.

Il y a beaucoup plus de paramètres pour personnaliser, mais vous avez l'idée. Je vous montrerai un exemple plus complexe plus tard.

Alors, comment l'exécuter ? Vous pourriez le déclencher manuellement via l'interface web d'Argo, la CLI, ou kubectl, mais ce n'est pas ce que nous cherchons. Pour automatiser cela, nous pouvons configurer des déclencheurs pour les workflows via Argo Events ou l'API REST d'Argo, ce qui ouvre des configurations intéressantes.

Un CronWorkflow de base

Cependant, dans notre quête pour remplacer GitHub Actions, il existe une autre méthode : le CronWorkflow. Comme son nom l'indique, c'est un workflow qui s'exécute selon un calendrier. Je préfère cette option car elle me permet d'éviter une source d'événements externe.

J'ai configuré le mien pour qu'il s'exécute toutes les 5 minutes pour construire et pousser les nouvelles images. J'ai choisi cette approche plutôt qu'Argo Events car elle me permet de retirer les GitHub Actions/webhooks du chemin critique et de garder la configuration autonome. Ce n'est pas nécessairement la solution la plus efficace.

Naturellement, je ne veux pas construire toutes les 5 minutes, car cela créerait une charge très lourde sur mon cluster. Cependant, il y a une solution simple : vérifier simplement si vous avez besoin de construire. Nous explorerons comment implémenter cette vérification dans les sections suivantes.

Notez que vous pouvez aussi les déclencher manuellement en plus de leurs exécutions planifiées via l'interface web ou la CLI.

Voici un exemple de CronWorkflow :

apiVersion: argoproj.io/v1alpha1
kind: CronWorkflow
metadata:
  name: hello-world-cron
  namespace: argo-builds
spec:
  schedules:
    - "* * * * *"
  concurrencyPolicy: Forbid
  workflowSpec:
    entrypoint: hello
    templates:
      - name: hello
        container:
          image: alpine:latest
          command: [sh, -c]
          args: ["echo 'Hello World'"]

C'est presque la même chose que ce que nous avions auparavant. Seules quelques choses changent :

  • name: change en CronWorkflow
  • concurrencyPolicy: Que se passe-t-il si un nouveau job veut démarrer alors qu'un autre n'est pas terminé ? Vous pouvez Allow (autoriser), Forbid (attendre), ou Replace (tuer l'ancien).
  • schedules: définit une liste de planifications via des expressions Cron, vous pouvez donc avoir un workflow complexe multi-planifié si nécessaire.

Si vous ne le savez pas, les expressions Cron fonctionnent comme ceci :

* * * * *
│ │ │ │ │
│ │ │ │ └─── Jour de la semaine (0 - 6, où 0 est Dimanche)
│ │ │ └───── Mois (1 - 12)
│ │ └─────── Jour du mois (1 - 31)
│ └───────── Heure (0 - 23)
└─────────── Minute (0 - 59)

Exemples :

  • "0 12 * * *" # S'exécute tous les jours à 12:00
  • "0 0 * * 1" # S'exécute tous les lundis à 00:00
  • "*/5 * * * *" # S'exécute toutes les 5 minutes

L'architecture du pipeline de build

Maintenant, construisons un vrai pipeline de CI avec cela. Mon pipeline de CI de base a trois étapes :

  1. Récupérer le Hash : Je récupère le dernier hash de commit de la branche cible sans télécharger les fichiers du repos. C'est rapide et ça évite du travail inutile.
  2. Vérifier le Cache : J'interroge mon registre de conteneurs pour vérifier si une image avec le tag du hash de commit existe déjà. Si l'image existe, le pipeline saute la construction. De cette façon, je ne construis pas constamment des images, mais seulement lorsque le repos Git est mis à jour.
  3. Construire : Si le hash a changé, je construis l'image Docker.

À des fins de débogage, je définis également une stratégie TTL pour nettoyer automatiquement les workflows terminés :

  • Les workflows qui réussissent sont supprimés après 180 secondes (3 minutes).
  • Les workflows qui échouent sont conservés pendant 3600 secondes (1 heure) pour le débogage.
    • Rappel sur les Quotas de Ressources : Comme je l'ai expliqué plus haut, si vous avez défini une limite de jobs dans vos quotas de ressources, cela peut entraîner une pénurie dans le namespace. Un job en échec qui ne fait rien compte toujours comme un job, ce qui peut empêcher de nouveaux jobs de s'exécuter.

Créer des Cluster Workflow Templates

Comme je réutilise le même pipeline de CI partout, faire des copier-coller serait un gaspillage. La solution d'Argo ici est le WorkflowTemplate : définissez le pipeline une fois, réutilisez-le partout. Vous pouvez utiliser ClusterWorkflowTemplate pour une utilisation entre différents espaces de noms, mais comme tous nos workflows sont dans argo-builds, nous n'en avons pas besoin.

Un WorkflowTemplate ressemble à un workflow normal, sauf qu'il n'est pas destiné à être exécuté seul. La principale différence ? Ils n'utilisent pas generateName car ils doivent être référencés par leur nom.

Voici le workflow que j'ai décrit plus tôt. Il est complexe (lisez-le si vous osez), mais c'est un parfait exemple de la puissance d'Argo.

Les éléments notables sont :

  • arguments: Ce sont les variables passées dans le template (nous y reviendrons).
  • volumes et containerSet: Nous montons des volumes emptyDir sur /workspace et /tmp. Dans les workflows Argo standards, les étapes s'exécutent dans des pods séparés, ce qui signifie que vous devez jongler avec des sidecars ou pousser des données vers un stockage juste pour partager des fichiers. Avec mon containerSet, plusieurs conteneurs s'exécutent dans le même Pod. Cela leur permet de partager un état localement — comme écrire le git sha dans /tmp/sha ou partager le repos dans /workspace/src sans toute cette surcharge.
  • Exécution Conditionnelle: L'étape 2 (check-registry) écrit true ou false dans /tmp/build_needed. L'étape 3 (buildkit-build) vérifie ce fichier à l'exécution : if [ "$(cat /tmp/build_needed)" = "false" ]; then exit 0; fi.

Ce n'est pas le plus beau workflow que vous verrez, mais il fonctionne. Le voici :

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: build-template
spec:
  # Define default values that can be overridden by the calling Workflow / CronWorkflow
  arguments:
    parameters:
      - name: repo-url
        value: "github.com/Local-pie/homepage.git"
      - name: git-branch
        value: "main"
      - name: image-name
        value: "zot.home.arpa/homepage"
      - name: image-tag-prefix
        value: "preprod"
      - name: target-platform
        value: "linux/arm64"
  ttlStrategy:
    secondsAfterSuccess: 180
    secondsAfterFailure: 3600
  entrypoint: build-pipeline
  volumes:
    - name: workspace
      emptyDir: {}
    - name: tmp
      emptyDir: {}
  templates:
    - name: build-pipeline
      containerSet:
        volumeMounts:
          - mountPath: /workspace
            name: workspace
          - mountPath: /tmp
            name: tmp
        containers:
          # 1. Instantly fetch the latest commit hash without downloading files
          - name: get-sha
            image: docker.io/alpine/git:v2.54.0
            resources:
              requests:
                cpu: "10m"
              limits:
                cpu: "50m"
            env:
              - name: GIT_USERNAME
                valueFrom:
                  secretKeyRef:
                    name: "github-org-pat-creds"
                    key: username
              - name: GIT_PASSWORD
                valueFrom:
                  secretKeyRef:
                    name: "github-org-pat-creds"
                    key: password
            command: [sh, -c]
            args:
              - |
                git ls-remote https://${GIT_USERNAME}:${GIT_PASSWORD}@{{workflow.parameters.repo-url}} {{workflow.parameters.git-branch}} | awk '{print $1}' > /tmp/sha
                echo "Target Hash: $(cat /tmp/sha)"
 
          # 2. Check if this hash already exists in the registry
          - name: check-registry
            image: quay.io/skopeo/stable:latest
            resources:
              requests:
                cpu: "10m"
              limits:
                cpu: "50m"
            dependencies: [get-sha]
            command: [sh, -c]
            args:
              - |
                COMMIT_HASH=$(cat /tmp/sha)
                TAG_NAME="{{workflow.parameters.image-tag-prefix}}-$COMMIT_HASH"
                echo "Checking if tag $TAG_NAME exists..."
                if skopeo inspect --tls-verify=false docker://{{workflow.parameters.image-name}}:$TAG_NAME > /dev/null 2>&1; then
                  echo "Tag exists. Skipping build."
                  echo "false" > /tmp/build_needed
                else
                  echo "Tag missing. Triggering build."
                  echo "true" > /tmp/build_needed
                fi
 
          # 3. Build and push using BuildKit
          - name: buildkit-build
            image: moby/buildkit:v0.19.0
            securityContext:
              privileged: true
            resources:
              requests:
                cpu: "200m"
              limits:
                cpu: "1"
            dependencies: [check-registry]
            env:
              - name: GIT_USERNAME
                valueFrom:
                  secretKeyRef:
                    name: "github-org-pat-creds"
                    key: username
              - name: GIT_PASSWORD
                valueFrom:
                  secretKeyRef:
                    name: "github-org-pat-creds"
                    key: password
            command: [sh, -c]
            args:
              - |
                if [ "$(cat /tmp/build_needed)" = "false" ]; then
                  echo "Skipping buildkit build."
                  exit 0
                fi
 
                # Extract registry domain dynamically for insecure configuration
                REGISTRY_DOMAIN=$(echo "{{workflow.parameters.image-name}}" | cut -d'/' -f1)
 
                # Create buildkitd config for insecure registry access
                mkdir -p /etc/buildkit
                cat > /etc/buildkit/buildkitd.toml <<EOF
                [registry."$REGISTRY_DOMAIN"]
                  http = true
                  insecure = true
                EOF
 
                # Start buildkitd in background with config
                nohup buildkitd --oci-worker-snapshotter=native --config /etc/buildkit/buildkitd.toml > /tmp/buildkitd.log 2>&1 &
                BUILDKITD_PID=$!
 
                echo "Started buildkitd (PID: $BUILDKITD_PID), waiting for readiness..."
 
                # Wait up to 60s for daemon socket to appear and be ready
                READY=false
                for i in $(seq 1 60); do
                  if [ -S /run/buildkit/buildkitd.sock ] && buildctl connections 2>/dev/null | head -n1 >/dev/null; then
                    echo "BuildKit daemon is ready after ${i}s."
                    READY=true
                    break
                  fi
                  sleep 1
                done
 
                if [ "$READY" = false ]; then
                  echo "ERROR: BuildKit daemon failed to start!"
                  cat /tmp/buildkitd.log
                  exit 1
                fi
 
                # Clone the repo into a local directory
                git clone https://${GIT_USERNAME}:${GIT_PASSWORD}@{{workflow.parameters.repo-url}} /workspace/src --depth 1 --branch {{workflow.parameters.git-branch}} \
                  2>&1 || { echo "ERROR: git clone failed"; exit 1; }
 
                COMMIT_HASH=$(cd /workspace/src && git rev-parse HEAD)
                echo "$COMMIT_HASH" > /tmp/sha
 
                # Write build command using the parameters (Using standard string concatenation instead of printf %% to avoid shell parsing bugs)
                echo "buildctl build --frontend=dockerfile.v0 --local context=/workspace/src --local dockerfile=/workspace/src --opt filename=Dockerfile --opt platform={{workflow.parameters.target-platform}} --opt provenance=false --output \"type=image,name={{workflow.parameters.image-name}}:{{workflow.parameters.image-tag-prefix}}-$COMMIT_HASH,push=true,compression=gzip,oci-mediatypes=true\" --import-cache type=registry,ref={{workflow.parameters.image-name}}/cache --export-cache type=registry,mode=max,ref={{workflow.parameters.image-name}}/cache,oci-mediatypes=true" > /tmp/build.sh
 
                # Run build for parameterized tag
                sh /tmp/build.sh 2>&1 || exit 1
 
                # Retag and push prefix-latest using the cached build
                sed "s/{{workflow.parameters.image-tag-prefix}}-.\*/{{workflow.parameters.image-tag-prefix}}-latest/g" /tmp/build.sh > /tmp/build_latest.sh
                sh /tmp/build_latest.sh 2>&1 || exit 1

Exemple de CronWorkflow pour le CI/CD

Maintenant que nous avons un template, nous pouvons le réutiliser dans plusieurs projets. Il accepte des paramètres pour l'URL du repos, la branche git, le nom de l'image, le préfixe du tag de l'image et la plateforme cible. C'est ainsi que je l'utilise en pratique dans tous mes projets où je dois construire des images.

Comparé au bazar ci-dessus, cela semble vraiment propre et clair :

apiVersion: argoproj.io/v1alpha1
kind: CronWorkflow
metadata:
  name: {{ .Values.app.namespace }}-build
  namespace: argo-builds
spec:
  schedule: "*/5 * * * *"
  concurrencyPolicy: Forbid
  workflowSpec:
    workflowTemplateRef:
      name: build-template
    arguments:
      parameters:
        - name: git-branch
          value: "{{ .Values.git.branch }}"
        - name: image-tag-prefix
          value: "{{ .Values.image.prefix }}"
        - name: repo-url
          value: "github.com/Local-pie/the-thought-process.git"
        - name: image-name
          value: "zot.home.arpa/thethoughtprocess"

Vous pouvez surcharger n'importe quel paramètre lorsque vous créez un CronWorkflow qui référence ce template. Si vous omettez un paramètre, il utilisera la valeur par défaut.

J'aime enregistrer mes workflows dans le même dossier k8s qui est surveillé par Argo CD. Ainsi, si je mets à jour le workflow, Argo CD le met à jour. Il apparaît également dans l'interface d'Argo CD :

Une capture d'écran d'un tableau de bord Argo CD montrant une synchronisation réussie d'une application nommée 'homepage' avec diverses ressources Kubernetes, avec 'homepage-build-cron' entouré en rouge.

Vous devriez également le voir dans l'interface d'Argo Workflow, listé sous Cron Workflow :

Une capture d'écran du tableau de bord d'Argo Workflows affichant une liste de cron workflows configurés dans le namespace argo-builds.

Vous pouvez aussi voir comment il s'exécute :

Une capture d'écran de l'interface utilisateur d'Argo Workflows affichant une exécution de workflow réussie.

Étape 3 : Installer Argo CD Image Updater

Nous approchons de la fin, mais il nous manque une dernière étape.

Nous pourrions forcer la mise à jour de l'image déployée en utilisant des workflows post-build, mais cela brise le modèle GitOps en contournant Git. À la place, ArgoCD Image Updater automatise ce processus nativement. Il interroge notre registre de conteneurs pour de nouvelles images et réécrit les nouveaux tags directement dans Git, restaurant ainsi le flux GitOps.

Son fonctionnement est simple : l'updater surveille votre registre pour de nouveaux tags et met à jour automatiquement les images déployées. Mettons cela en place.

Installer l'Image Updater

D'abord, nous installons Argo Image Updater

helm install argocd-image-updater argo/argocd-image-updater --namespace argocd

Si vous avez suivi ma configuration de Zot auto-hébergé, vous devrez également surcharger la ConfigMap pour autoriser le pulling depuis votre Zot non sécurisé. Par exemple :

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-image-updater-config
  namespace: argocd
data:
  registries.conf: |
    registries:
    - name: Zot
      api_url: http://zot.home.arpa
      prefix: zot.home.arpa
      insecure: true

Comment ça marche

Annotations de l'Image Updater

Argo Image Updater fait le pont entre votre processus de build et Argo CD. Il surveille votre registre de conteneurs pour de nouvelles images et met automatiquement à jour les tags des images dans vos manifestes d'Application Argo CD.

Pour activer cela, il vous suffit d'ajouter une annotation à votre manifeste d'Application Argo CD spécifiant la stratégie de mise à jour :

metadata:
  annotations:
    argocd-image-updater.argoproj.io/image-list: my-app-registry=zot.home.arpa/my-app-registry
    argocd-image-updater.argoproj.io/my-app-registry.update-strategy: newest-build
    argocd-image-updater.argoproj.io/my-app-registry.allow-tags: regexp:^prd-[a-f0-9]+$
    argocd-image-updater.argoproj.io/write-back-method: argocd

Laissez-moi expliquer les parties clés :

  • image-list: Définit quel est le repos de votre registre d'images d'application.
  • update-strategy: Définit quelle image utiliser. Ici, newest-build indique l'image la plus récente.
  • allow-tags: Regex pour filtrer les images avec des noms comme prd-a1b2c3....
    • Note : L'expression régulière allow-tags exige que le tag dans votre manifeste de déploiement commence par prd-. Si votre manifeste utilise latest, l'updater l'ignorera car latest ne correspond pas à votre regex. Vous devez utiliser un placeholder modifiable comme prd-a1b2c3.
  • write-back-method: Définit comment l'updater commit le changement.
    • git : Le changement est commité dans Git, et Argo CD est censé le récupérer. Git reste la source de vérité, mais cela ajoute des commits automatisés à votre historique. Incompatible avec ma stratégie de CronWorkflow, car un nouveau commit déclencherait un nouveau build, menant ainsi à une boucle infinie.
    • argocd : Nous demandons à Argo de mettre à jour l'image directement. Pas de commits automatisés, mais Git n'est plus exactement la source de vérité.

Ces annotations sont placées dans votre manifeste d'application Argo CD comme ceci :

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  annotations:
    argocd-image-updater.argoproj.io/image-list: my-app-registry=zot.home.arpa/my-app-registry
    argocd-image-updater.argoproj.io/my-app-registry.update-strategy: newest-build
    argocd-image-updater.argoproj.io/my-app-registry.allow-tags: regexp:^prd-[a-f0-9]+$
    argocd-image-updater.argoproj.io/write-back-method: argocd
  name: homepage
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/Local-pie/homepage.git
    targetRevision: main
    path: k8s
  destination:
    server: "https://kubernetes.default.svc"
    namespace: utils-dev
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Déploiement multi-images

Si vous avez plusieurs images dans votre déploiement, vous pouvez configurer Argo CD Image Updater pour les gérer séparément. Dans cet exemple, j'utilise digest pour le backend, ce qui indique à l'updater de surveiller les changements dans le digest de l'image plutôt qu'un tag spécifique.

metadata:
  annotations:
    # 1. Define multiple images using aliases separated by commas
    argocd-image-updater.argoproj.io/image-list: fe=zot.home.arpa/my-app-registry/frontend, be=zot.home.arpa/my-app-registry/backend
 
    # --- Frontend Rules (alias: fe) ---
    argocd-image-updater.argoproj.io/fe.update-strategy: newest-build
    argocd-image-updater.argoproj.io/fe.allow-tags: regexp:^prd-fe-[a-f0-9]+$
 
    # --- Backend Rules (alias: be) ---
    argocd-image-updater.argoproj.io/be.update-strategy: digest
    argocd-image-updater.argoproj.io/be.allow-tags: regexp: dev
 
    # --- Global Settings ---
    argocd-image-updater.argoproj.io/write-back-method: argocd

Pour être complet, votre déploiement associé ressemblerait à ceci :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        # Frontend Container
        - name: frontend
 
          #Notice the placeholder `prd-fe-1234567`
          #It doens't point to any image
          #but respect the expected format in our regex
          #Otherwise, Image Updater won't be happy
          image: zot.home.arpa/my-app-registry/frontend:prd-fe-1234567
          ports:
            - containerPort: 80
 
        # Backend Container
        - name: backend
          image: zot.home.arpa/my-app-registry/backend:dev
          ports:
            - containerPort: 8080

Débogage de l'Image Updater

L'Image Updater est le seul composant sans un bon retour visuel car nous n'avons ni interface utilisateur ni intégration dans l'UI d'Argo CD. Il fonctionne simplement en arrière-plan. S'il y a des problèmes, la meilleure façon est de regarder les logs.

La boucle GitOps complète

À ce stade, nous avons les trois pièces du puzzle :

  • Argo CD déploie les manifestes Kubernetes définis dans Git.
  • Argo Workflows construit et pousse nos images de conteneur.
  • Argo CD Image Updater surveille le registre pour de nouvelles images.

Voici comment ca marche en résumé.

1. Un commit est poussé sur Git

Tout commence par une modification du code source de l'application. Par exemple, je modifie mon application et je pousse les changements sur la branche main.

2. Argo CD détecte le changement

Argo CD surveille continuellement le repos et détecte que l'état souhaité a changé. Si les manifestes Kubernetes eux-mêmes ont changé, Argo CD peut immédiatement les synchroniser avec le cluster. Cependant, pour les changements de code applicatif, nous devons encore construire une nouvelle image de conteneur. C'est là qu'intervient Argo Workflows.

3. Argo Workflow construit l'image

Notre CronWorkflow vérifie périodiquement le repos pour de nouveaux commits. Il récupère le dernier hash de commit et vérifie si une image pour ce commit existe déjà dans le registre. Si ce n'est pas le cas, le workflow construit l'image et la pousse sur Zot.

Par exemple :

zot.home.arpa/homepage:preprod-8f31c2a

À ce stade, la nouvelle image existe dans le registre.

4. La nouvelle image devient la nouvelle version

Argo CD Image Updater surveille le registre pour de nouvelles images et met directement à jour la référence d'image utilisée par l'application.

Et ensuite ?

Nous avons terminé ! Je ne peux pas dire que c'était aussi facile que de configurer un Runner GitHub auto-hébergé, mais ce n'était pas si dur.

L'écosystème Argo est assez modulaire et si vous voulez aller plus loin, vous pouvez explorer :

Judicael Poumay (Ph.D.)

Judicael Poumay (Ph.D.)

Suivez-moi sur LinkedIn pour du contenu hebdomadaire Judicaël Poumay

En tant que chercheur/développeur IA indépendant spécialisé en Traitement du Langage Naturel (NLP), j'ai une expertise complète dans le développement et l'intégration de systèmes d'IA, ainsi que l'analyse de données.

Votre entreprise cherche à intégrer des solutions IA, analyser des données ou renforcer son développement back-end ? Contactez-moi !

Offrez-moi une bière 🍺

Articles Similaires