Aller au contenu

Tuto · pour qui en a marre du devops

ArgoCD + K8s pour les nuls

Si tu as ouvert cette page c'est probablement parce que tu te demandes pourquoi diable il faut autant de YAML par service, pourquoi ArgoCD existe quand kubectl apply marche très bien, et pourquoi un pod a planté à 22h hier sans que tu comprennes. Cette page raconte tout ça en partant des fondamentaux et en s'appuyant sur le projet maritime-atlas (apps perso : tracker AIS + météo marine, 17 services en prod K8s). Lecture ~25 min. Si tu débutes carrément K8s, voir aussi /k8s-for-java-developers qui couvre l'introduction côté dev Java.

0. Pour qui est ce tuto

Tu es développeur. Tu sais coder. Tu déployais ton appli avec un scp app.jar prod: + systemctl restart et ça marchait très bien. Maintenant on te dit qu'il te faut un cluster Kubernetes, qu'il faut écrire 8 fichiers YAML pour déployer un seul service, qu'il faut installer ArgoCD, Helm, sealed-secrets, cert-manager, et tu te demandes si l'industrie s'est pas un peu emballée.

Spoiler : non, ils se sont pas emballés. L'écosystème a juste explosé pour répondre à 4 besoins très concrets qu'on découvre dès qu'on met des trucs en prod. Cette page t'explique pourquoi chaque couche existe, avec des analogies que tu connais déjà. Pas pour t'apprendre l'API K8s par coeur — pour que demain matin, quand tu rouvres ArgoCD, tu comprennes ce que tu lis.

Hypothèse de base : tu as déjà utilisé Docker (docker run, docker-compose) et tu sais ce qu'est une image. Si non, commence par là — K8s c'est de l'orchestration de containers Docker, donc sans la brique du dessous c'est dur.

1. Pourquoi tant de YAML ? La vraie réponse

La question légitime quand on débarque sur K8s : "j'ai un seul container Node.js à faire tourner, pourquoi 8 fichiers YAML ?"

Réponse : parce qu'un service en production a 4 jobs séparés, et chaque job mérite son propre fichier (déclaratif, reviewable en PR, modifiable indépendamment).

  • Job 1 — Faire tourner le code. Combien de copies, quelle image Docker, quelles ressources CPU/RAM, comment redémarrer quand ça crash. → Deployment.yaml
  • Job 2 — Être joignable depuis d'autres services. DNS interne stable, load-balancing entre les copies. → Service.yaml
  • Job 3 — Être joignable depuis Internet (si c'est un frontend ou une API publique). Routing par host, HTTPS, certificats. → Ingress.yaml + Certificate.yaml
  • Job 4 — Configuration externalisée (URL de la DB, clés API, feature flags). Le code est figé dans l'image, la config varie selon l'env. → ConfigMap.yaml + Secret.yaml

Ajoute "stockage persistant" (PVC), "scheduled tasks" (CronJob), "permissions cluster" (ServiceAccount + RBAC), et tu arrives à ~8 fichiers. C'est infrastructure-as-code reviewable : chaque préoccupation a son fichier, ton équipe peut review en PR comme du code applicatif.

L'erreur de débutant c'est de comparer K8s à docker run --rm -p 3000:3000 monapp. Compare-le plutôt à "Docker + nginx + systemd + Ansible + dotenv + cert-manager + supervisor" tous réunis sous une API déclarative. Là c'est plus juste.

2. Le mental model en 1 minute

4 mots à retenir, dans cet ordre :

  • Pod = un (ou plusieurs) container Docker qui tourne. C'est l'unité atomique. Quand on parle d'une "copie de l'appli qui tourne", c'est un pod.
  • Deployment = le cahier des charges qui dit "je veux 2 pods de l'image maritime/api:v1.4.2 en permanence". Si un pod meurt, K8s en recrée un. Si on change l'image, il fait un rolling update (1 nouveau pod up → 1 ancien tué → suivant).
  • Service = un balanceur DNS interne. Tu as 2 pods derrière, mais les autres services tapent http://api:3010 et K8s load-balance. Le pod peut être recréé 10 fois par jour, son IP change, mais le Service garde la même adresse.
  • Ingress = la porte d'entrée HTTPS depuis Internet. Règle déclarative "host aetherwx.sladoire.dev → Service maritime-frontend port 80". Le contrôleur Ingress (nginx en backstage) applique + cert TLS auto via cert-manager.
Les couches (du bas vers le haut) text
           Internet
              |
              v
        [ Ingress ]
"aetherwx.sladoire.dev"
              |
              v
       [ Service "api" ]
     (DNS + load-balancer)
          /         \
         v           v
     [ Pod ]      [ Pod ]
     api-abc      api-def
        |           |
        v           v
    container    container
     node.js      node.js
        (image v1.4.2)

Tout le reste (ConfigMap, Secret, PVC, ServiceAccount, etc.) c'est des compléments qu'on injecte dans le Pod. Mais le mental model de base c'est ces 4 briques.

3. Helm = les templates qui sauvent ta santé mentale

Imagine que tu as 14 services, chacun avec ses ~8 fichiers YAML. Ça fait 112 fichiers quasi-identiques (90% du Deployment d'un service ressemble à 99% du Deployment du voisin, juste l'image change et 3 env vars).

Pire : tu veux faire un environnement preprod et un prod, donc c'est x2 = 224 fichiers. C'est ingérable.

Helm c'est juste "templating + values" pour K8s. Tu écris UNE fois la structure (template) avec des placeholders, et tu fournis un values.yaml par environnement.

charts/maritime/templates/deployment.yaml yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.name }}
spec:
  replicas: {{ .Values.replicas }}
  template:
    spec:
      containers:
        - name: app
          image: {{ .Values.image.repo }}:{{ .Values.image.tag }}
          resources:
            limits:
              memory: {{ .Values.resources.memory }}
charts/maritime/values.yaml yaml
name: maritime-api
replicas: 2
image:
  repo: ghcr.io/sylad/maritime-api
  tag: v1.4.2
resources:
  memory: 512Mi

Bump le tag dans values.yaml, helm upgrade, K8s applique le rolling update. 1 template = N déploiements. Sur maritime-atlas le chart Helm fait ~9 templates et génère ~130 manifests K8s à l'install.

Analogie : c'est comme un Maven pom.xml parent avec profiles, ou un package npm scopé que tu paramètres depuis l'extérieur. Sauf qu'au lieu de produire un .jar, ça produit du YAML K8s.

4. GitOps = stop kubectl apply à la main

Avant GitOps, le workflow K8s c'était :

  1. SSH sur ta machine de dev
  2. Éditer les YAML
  3. kubectl apply -f deploy.yaml
  4. Croiser les doigts
  5. Oublier de commit les modifs
  6. 2 mois plus tard, drift entre Git et le cluster, plus personne ne sait quelle version tourne vraiment

GitOps inverse le flux : Git devient la source de vérité unique. Tu ne fais plus jamais kubectl apply. Tu commits dans un repo (chez moi : developpeur-gitops), et un agent installé dans le cluster détecte le commit et applique automatiquement. Cet agent c'est ArgoCD.

Le flux GitOps maritime-atlas text
[1] code Big-Blue (WSL)
       |
       v  git push main
[2] GitHub repo "maritime-atlas"
       |
       v  GitHub Actions
[3] build Docker + push ghcr.io/sylad/maritime-api:sha-abc1234
       |
       v  workflow bump-tag
[4] commit dans "developpeur-gitops" → values.yaml: tag = sha-abc1234
       |
       v  webhook GitHub → ArgoCD (serveur de prod dark-blue)
[5] ArgoCD detect diff entre Git et cluster
       |
       v  helm template + kubectl apply (que ArgoCD fait pour toi)
[6] K8s rolling update : pods anciens tués, nouveaux up avec :sha-abc1234
       |
       v
[7] users voient nouvelle version, zéro downtime

Total : ~3 min (build) + 15 sec (sync ArgoCD via webhook).

Bénéfices concrets que tu touches du doigt après 2 semaines :

  • Audit trail : "qui a déployé v1.4.2 et quand ?" → git blame values.yaml.
  • Rollback en 30 sec : git revert + push, ArgoCD réapplique l'ancien état.
  • Disaster recovery : cluster cramé ? Réinstalle K8s vierge + ArgoCD + pointe sur le repo. ArgoCD reconstruit tout. Vécu sur maritime quand j'ai migré dual-cluster → single-cluster.
  • Selfheal : un collègue fait kubectl edit à la main pour "tester un truc" ? ArgoCD détecte le drift et réverte dans les 3 min. Annoying au début, vital en prod.

5. ArgoCD au quotidien (UI tour)

Quand tu ouvres https://argocd.aetherwx.sladoire.dev, tu vois une grille d'Applications (chaque Application = un chart Helm watché par ArgoCD). 3 états possibles, dans 2 dimensions :

  • Sync status : Synced (Git = cluster), OutOfSync (Git a divergé, ArgoCD va appliquer dans les 3 min sauf si autosync off).
  • Health status : Healthy (tous les pods running, probes OK), Progressing (rolling update en cours), Degraded (au moins un pod en crashloop ou un Job qui failed).

3 actions essentielles à connaître :

  • Click sur l'app → tu vois le graphe de toutes les ressources (Deployments, Pods, Services, Ingress). Visuel, ça aide énormément à comprendre les relations.
  • Click sur un Pod → onglet "Logs" pour voir stdout/stderr. Onglet "Events" pour voir pourquoi K8s a tué le pod (OOMKilled, liveness probe failed, image pull error...).
  • Bouton "Sync" en haut → force l'apply immédiat sans attendre les 3 min de polling. Pratique pour débloquer ou debug.

Tu peux tout faire en CLI aussi (argocd app sync maritime, argocd app get maritime), mais l'UI bat la CLI pour 90% des usages quotidiens.

6. Pipeline complet maritime-atlas

Voici concrètement ce qui se passe quand je modifie une ligne de code TypeScript dans maritime/services/api/src/health.ts et que je push :

  1. Big-Blue (mon PC) : git push origin main dans le repo maritime-atlas.
  2. GitHub Actions détecte le push, lance le workflow build-and-push.yml :
    • npm ci + npm run build
    • docker build -t ghcr.io/sylad/maritime-api:sha-abc1234
    • docker push ghcr.io/sylad/maritime-api:sha-abc1234
    • Workflow bump-tag : git clone developpeur-gitops, modifie charts/maritime/values.yaml (champ api.image.tag), commit + push.

    Total ~3 min selon la taille du build.

  3. Webhook GitHub → ArgoCD : GitHub pousse une notif HTTP à ArgoCD dès que le repo gitops change. ArgoCD détecte le diff en ~15 sec (sans webhook : polling 3 min).
  4. ArgoCD sync : helm template avec le nouveau values.yaml → diff avec l'état cluster → kubectl apply des changes. Le seul diff c'est l'image du Deployment.
  5. K8s rolling update : K8s tue les pods avec l'ancien tag un par un, démarre les nouveaux. Si la liveness probe répond, il continue. Si elle fail, il rollback auto.
  6. Zéro downtime côté users si le Service avait ≥2 pods (et c'était le cas).

Ce que tu remarques : je n'ai jamais touché à un serveur. Pas de SSH. Pas de kubectl apply. Juste un git push + un workflow GitHub qui bump le tag. Le reste est automatique.

7. Incident vécu #1 — Le CronJob retention qui a tué ArgoCD pendant 48h

Hier soir, ArgoCD passe l'application maritime en Degraded sans raison apparente. Tous les pods Deployment sont Healthy. Le frontend marche. L'API marche. Mais ArgoCD dit Degraded.

Je clique pour voir le détail. ArgoCD pointe un CronJob : maritime-retention-cleanup (le job qui purge les données obs > 7 jours, cf politique data layer). Il tourne tous les jours à 3h du mat. Le dernier run a échoué avec exit code 1.

Investigation bash
# Lister les jobs de ce CronJob
kubectl get jobs -n maritime | grep retention

# Logs du dernier job échoué
kubectl logs -n maritime job/maritime-retention-cleanup-29384567
# date: invalid date '7 days ago'
# date: invalid date '7 days ago'
# date: invalid date '7 days ago'
# ...

La cause : mon script CronJob fait date -d '7 days ago' +%Y-%m-%d pour calculer la date de cutoff. Ça marche très bien sur GNU date (mon WSL, Big-Blue). Mais l'image de base du CronJob c'est alpine:3.20, qui utilise BusyBox date. BusyBox ne supporte PAS l'option -d avec syntax relative. Il faut date -D '%s' -d "$(($(date +%s) - 604800))" ou utiliser coreutils.

Pourquoi ArgoCD est passé Degraded : ArgoCD considère un CronJob avec son dernier Job en Failed comme une ressource unhealthy. Donc tout le chart maritime apparaît Degraded, même si l'API et le frontend tournent nickel.

Fix : remplacer Alpine par debian:bookworm-slim dans le CronJob (10 lignes Dockerfile), ou utiliser apk add coreutils avant d'invoquer date.

Leçon transversale : en Docker/K8s, "ça marche sur ma machine" inclut la libc et les coreutils. Alpine (musl + BusyBox) ≠ Debian (glibc + GNU). Si ton script utilise des options non-POSIX, soit tu installes les outils GNU, soit tu changes de base. Et toujours surveiller les CronJobs : ils peuvent fail silencieusement pendant des jours.

8. Incident vécu #2 — Sprint 0 rebrand, 9 images manquantes

Sprint 0 du rebrand maritime → aetherwx. J'ai changé le préfixe des images Docker de maritime-* vers aetherwx-* dans le chart Helm + values.yaml. Push, commit gitops, ArgoCD sync.

ImagePullBackOff sur 9 pods. Logique : les images ghcr.io/sylad/aetherwx-* n'existaient pas encore — j'avais bumpé le YAML AVANT que la CI rebuilde les nouvelles images sous le nouveau nom. Classic.

Pourquoi ça arrive : dans un pipeline GitOps bien fait, tu as 2 repos : code (qui produit les images) et gitops (qui décrit ce qui tourne). Si tu fais un rebrand global, il faut le faire dans cet ordre :

  1. Modifier les workflows CI pour build sous le nouveau nom (dual-tag pour transition)
  2. Push code → workflow construit ET pousse les nouvelles images
  3. VÉRIFIER que les images existent sur GHCR (curl + GitHub UI)
  4. SEULEMENT APRÈS, modifier le repo gitops

Fix de l'urgence : workflow_dispatch manuel sur GitHub Actions pour relancer les builds, puis bump SHA dans values.yaml pour pointer sur les nouvelles images.

Leçon transversale : GitOps introduit un découplage entre "le code est buildé" et "le code est déployé". Les deux peuvent se désynchroniser. Toujours bumper le gitops après avoir confirmé que l'image cible existe.

9. Cheat sheet kubectl + argocd

Les ~15 commandes qui couvrent 90% du quotidien. Copy-paste friendly, garde cette section ouverte dans un onglet.

kubectl essentiels bash
# Voir ce qui tourne dans un namespace
kubectl get pods -n maritime
kubectl get pods -n maritime -o wide          # + node + IP
kubectl get pods -A | grep -v Running         # tout ce qui n'est pas OK

# Logs d'un pod (ou suivre en live avec -f)
kubectl logs -n maritime maritime-api-7d9c8f4b6-abc12
kubectl logs -n maritime maritime-api-7d9c8f4b6-abc12 -f
kubectl logs -n maritime -l app=maritime-api --tail=200   # par label

# Pourquoi le pod est en CrashLoopBackOff ?
kubectl describe pod -n maritime maritime-api-7d9c8f4b6-abc12
kubectl get events -n maritime --sort-by='.lastTimestamp' | tail -30

# Exec dans un pod (comme docker exec -it)
kubectl exec -n maritime -it maritime-api-7d9c8f4b6-abc12 -- sh

# Restart un Deployment (utile si une env var ConfigMap a bougé)
kubectl rollout restart deployment/maritime-api -n maritime

# Forwarder un port local vers un pod (debug)
kubectl port-forward -n maritime svc/maritime-api 3010:3010

# Voir les ressources et leurs usages
kubectl top pods -n maritime
kubectl top nodes
argocd essentiels bash
# Login (1 fois par session)
argocd login argocd.aetherwx.sladoire.dev

# Lister les apps
argocd app list

# Voir le détail d'une app + sync status
argocd app get maritime

# Forcer un sync (au lieu d'attendre les 3 min de polling)
argocd app sync maritime

# Voir le diff entre Git et cluster
argocd app diff maritime

# Rollback à un commit précédent
argocd app rollback maritime <revision>

# Suspendre l'auto-sync (pour debug ponctuel)
argocd app set maritime --sync-policy none
# ... debug ...
argocd app set maritime --sync-policy automated --auto-prune --self-heal

10. Les 5 réflexes à acquérir en premier

  1. Ne jamais kubectl apply à la main en prod. Si tu le fais, ArgoCD va réverte dans les 3 min (selfHeal=true). Passe par Git. Toujours.
  2. Toujours bumper le tag d'image, jamais :latest. K8s avec imagePullPolicy: IfNotPresent (le défaut) ne re-pull pas. Utilise :sha-abc1234 ou :v1.4.2.
  3. Vérifier que l'image cible existe AVANT de bump le gitops. Workflow GitHub → vérifie le tag sur GHCR → bump values. Sinon ImagePullBackOff garanti.
  4. Lire les Events autant que les Logs. kubectl get events te dit pourquoi K8s a tué un pod (OOMKilled, probe failed, ImagePullBackOff). Les Logs te disent ce que l'app affichait juste avant. Les deux sont complémentaires.
  5. Quand ArgoCD passe Degraded, regarde TOUTES les ressources, pas que les Pods. Un CronJob qui fail, un Job qui n'a pas terminé, un Certificate en erreur — tout peut faire Degraded. L'UI ArgoCD affiche un cadenas rouge/orange sur la ressource problématique.

Conclusion · Ce que tu sais déjà vaut cher

Si tu maîtrises les concepts de cette page, tu sais déjà : infrastructure-as-code, déploiement continu, GitOps, observabilité de base, rolling updates, disaster recovery, multi-environnement via templating.

Sur le marché 2026, c'est un skillset que les boîtes mid-size (50-500 personnes) cherchent à payer 70-100k€/an. Pas parce que c'est ultra-complexe (tu vois bien, ça tient en 10 sections), mais parce que peu de devs ont mis les mains dedans end-to-end. La plupart connaissent kubectl mais pas le pipeline GitOps complet. La plupart connaissent Helm mais n'ont jamais debug un CronJob Alpine qui plante.

Si tu débutes en 2026 et que tu as un projet perso à migrer (même petit, une app perso, un site, n'importe quoi), fais le. Pas pour la techno en soi, mais pour avoir vécu une fois le cycle complet : code → CI → registry → gitops → sync → prod. Après ça, l'API K8s ce n'est plus qu'un manuel à feuilleter quand tu en as besoin.

Pour aller plus loin : K8s pour développeurs Java (mêmes concepts mais via les analogies Spring), et l'encyclopédie AetherWX qui détaille l'archi complète des 17 services avec diagrammes Mermaid. Les deux complètent cette page.