Aller au contenu

Module 3y · Le déclaratif - écrire ses manifestes (YAML)

Objectifs

  • Lire du YAML sans l'avoir jamais appris : trois briques, pas une de plus.
  • Connaître les rubriques d'un manifeste et savoir dire à quoi sert chaque ligne.
  • Écrire un Pod, un Deployment + Service, un ConfigMap injecté - en partant d'un squelette, comme dans la vraie vie.
  • Savoir lire et corriger un manifeste cassé (le quotidien de l'ops).
  • Durée : ~1 h 30 · Pré-requis : Module 3.

Pourquoi le déclaratif

Jusqu'ici on a tout fait à la main (kubectl run, create, scale…) : c'est l'impératif, parfait pour explorer vite. Mais rien n'en reste : ce que tu as déployé n'existe que dans ta tête et dans ton historique de commandes.

En production on DÉCRIT l'état voulu dans un fichier qu'on applique : c'est le déclaratif. Le fichier devient la référence - reproductible, versionnable, relisible en revue de code, et la base de GitOps (module 4e).

Impératif Déclaratif
Tu donnes un ordre (« crée 3 pods ») une cible (« je veux 3 pods »)
Ça vit dans l'historique du shell dans un fichier, dans Git
Rejouable ? à la main, en espérant se souvenir kubectl apply -f, à l'identique
Sert à explorer livrer

Les deux servent : on ne remplace pas l'un par l'autre.

La boucle : écris → applique → vérifie

kubectl apply -f mon-pod.yaml     # enregistre l'état désiré (crée OU met à jour)
kubectl get -f mon-pod.yaml       # vérifie ce que ça a donné
graph LR
    F[fichier YAML<br/>état DÉSIRÉ] -->|kubectl apply| API[(API server<br/>etcd)]
    API -->|contrôleurs| R[état RÉEL<br/>du cluster]
    R -.écart corrigé en continu.-> API

apply ne « lance » rien : il enregistre l'état désiré. Ce sont les contrôleurs qui font converger le réel vers ce qui est écrit, en permanence - exactement la réconciliation du module 1, mais cette fois la source est un fichier que tu versionnes.

create fabrique, apply réconcilie

kubectl create dit « fabrique cet objet » : si l'objet existe déjà, il refuse. kubectl apply dit « fais en sorte que cet objet soit comme ça » : il le crée s'il manque, le met à jour s'il existe, et ne se plaint jamais. On appelle ça l'idempotence : dix apply d'affilée sur le même fichier donnent le même résultat, sans erreur. C'est exactement ce qu'il faut pour un fichier versionné qu'on ré-applique en boucle - create pour bricoler, apply pour livrer.

YAML, avant même de parler de Kubernetes

Un manifeste s'écrit en YAML. Ce n'est pas une technologie Kubernetes : c'est juste une façon d'écrire de l'information structurée en texte, faite pour être lue par un humain. On en trouve partout (GitHub Actions, Ansible, docker compose…). Si tu n'en as jamais vu, deux minutes suffisent : il n'y a que trois briques.

prenom: Antoine            # (1)!
langues:                   # (2)!
  - français
  - anglais
adresse:                   # (3)!
  ville: Lille
  code_postal: "59000"     # (4)!
  1. Brique 1 - clé : valeur. Un nom, deux points, un espace, la valeur. C'est l'essentiel de tout fichier YAML.
  2. Brique 2 - une liste. Chaque élément commence par un tiret. Ici langues en contient deux.
  3. Brique 3 - l'imbrication. Tout ce qui est décalé vers la droite est à l'intérieur de la ligne du dessus. ville et code_postal sont dans adresse.
  4. Les guillemets forcent la lecture « texte ». Sans eux, 59000 serait un nombre - et un code postal qui commence par un zéro y perdrait son zéro.
Brique Ça s'écrit Ça veut dire
Clé / valeur nom: web l'information nom vaut web
Imbrication 2 espaces de décalage la ligne décalée est dans celle du dessus
Liste - français un élément parmi plusieurs

Il n'y a pas d'accolades ni de balises : le décalage EST la hiérarchie. C'est ce qui rend YAML agréable à lire… et impitoyable sur les espaces.

Comment on lit un YAML

Pose ton doigt sur une ligne et remonte vers la gauche : la première ligne moins décalée au-dessus est son « parent ». C'est tout le secret. Dans ta sandbox VS Code, l'extension indent-rainbow colore les niveaux : tu vois la hiérarchie sans compter les espaces.

Les trois pièges de forme (tous les débutants les prennent)

  1. Jamais de tabulation. La spécification YAML l'interdit pour l'indentation, point. 2 espaces par niveau, et c'est déjà réglé dans l'éditeur de ta sandbox. Une tabulation donne une erreur au parsing, avec la ligne et la colonne fautives : c'est tout ce dont tu as besoin pour la trouver.
  2. Quote tes valeurs ambiguës. En YAML, yes, no, on, off non entourés de guillemets sont lus comme des booléens (vrai/faux), et version: 1.20 est lu comme le nombre 1.2. Si une valeur doit rester du texte : "no", "1.20". Quand tu vois des guillemets dans un YAML de production, ce n'est pas de la décoration.
  3. Une liste d'objets, ça se lit en deux temps. Le motif qui déroute au début :

    containers:          # une liste…
      - name: web        # …dont le 1ᵉʳ élément est un objet : le tiret ouvre l'élément,
        image: nginx     #   et tout ce qui est aligné sous « name » appartient au MÊME élément
      - name: sidecar    # 2ᵉ élément
        image: busybox
    

    Le tiret et la clé qui le suit sont sur la même ligne ; les autres clés du même élément s'alignent sous cette clé, pas sous le tiret.

Deux détails qui font gagner du temps

  • Tout JSON valide est du YAML valide (YAML en est un sur-ensemble) : on peut donc écrire matchLabels: { app: web } sur une ligne, et tu croiseras les deux styles.
  • L'API Kubernetes, elle, ne parle que JSON : ton YAML est converti par kubectl avant l'envoi. Le YAML n'existe que pour tes yeux.

Anatomie d'un manifeste, ligne par ligne

Un manifeste, c'est un formulaire : les mêmes rubriques, toujours dans le même ordre, quel que soit l'objet. Voici le plus simple de tous - un Pod :

apiVersion: v1               # (1)!
kind: Pod                    # (2)!
metadata:                    # (3)!
  name: mon-pod
spec:                        # (4)!
  containers:                # (5)!
    - name: web
      image: nginx:1.27-alpine
  1. Dans quel catalogue on pioche. Kubernetes range ses objets par familles et par versions. v1 est le catalogue historique (Pod, Service, ConfigMap) ; les objets applicatifs, eux, vivent dans apps/v1 (Deployment, ReplicaSet, DaemonSet).
  2. Quel objet on veut. Toujours avec une majuscule : Pod, Deployment, Service, ConfigMap.
  3. Sa carte d'identité : son name (obligatoire), son namespace, et surtout ses labels - les étiquettes par lesquelles les autres objets le retrouveront.
  4. Ce que tu VEUX. C'est la seule rubrique qui change vraiment d'un type d'objet à l'autre : le spec d'un Pod ne ressemble pas à celui d'un Service.
  5. Une liste (le tiret) : un Pod peut porter plusieurs conteneurs. Ici un seul, nommé web, qui tourne sur l'image nginx:1.27-alpine.
Rubrique La question à laquelle elle répond
apiVersion dans quel catalogue d'objets je pioche ?
kind quel objet je crée ?
metadata comment il s'appelle, et quelles étiquettes il porte ?
spec ce que je veux qu'il soit

C'est la grille de lecture à retenir : elle vaut pour un Pod de 8 lignes comme pour un manifeste de 200 lignes trouvé sur internet. Un manifeste n'est jamais plus que ça, empilé.

La 5ᵉ rubrique, celle que tu n'écris jamais : status

Quand tu lis un objet existant (kubectl get pod mon-pod -o yaml), tu vois une rubrique en plus : status. C'est l'état réel, écrit par Kubernetes - jamais par toi. spec = ce que tu veux, status = ce qui est : le thermostat du module 1, noir sur blanc dans chaque objet. Tout le travail des contrôleurs consiste à réduire l'écart entre les deux.

« Presque » tous les objets

Quelques objets remplacent spec par une rubrique à eux : un ConfigMap et un Secret portent leurs valeurs sous data, un Role ses permissions sous rules. Les trois premières rubriques, elles, ne bougent jamais.

Dans metadata : labels vs annotations

Deux dictionnaires qui se ressemblent et qu'on confond tout le temps :

  • les labels servent à sélectionner : c'est par eux qu'un Service trouve ses pods, qu'un Deployment reconnaît les siens, que kubectl get -l app=web filtre ;
  • les annotations ne servent qu'à porter de l'information : une description, un propriétaire, un réglage lu par un outil (l'ingress, par exemple). Aucun selector ne les lit jamais.

La règle : si quelque chose doit retrouver ton objet, c'est un label ; si c'est juste une note collée dessus, c'est une annotation.

Comment on écrit un manifeste (pour de vrai)

La question qui angoisse tout le monde : « je dois retenir tout ça par cœur ? » Non. Personne n'écrit un manifeste de zéro, pas même les gens dont c'est le métier depuis dix ans. Le geste professionnel tient en quatre étapes, toujours les mêmes.

① Partir d'un squelette (jamais de la page blanche)

Trois façons d'en obtenir un, de la plus universelle à la plus courante :

La méthode qui marche partout, même en SSH sur un serveur sans éditeur. On reprend une commande impérative qu'on connaît déjà, et on ajoute deux options :

kubectl create deployment web --image nginx:1.27-alpine --dry-run=client -o yaml > web.yaml
  • --dry-run=client : « fais semblant, ne crée rien » ;
  • -o yaml : « montre-moi le YAML que tu aurais envoyé ».

Le résultat est un manifeste complet et valide, qu'il n'y a plus qu'à retravailler. C'est le réflexe n°1 du module : ton impératif du module 3 devient ta machine à fabriquer du YAML.

Dans la sandbox VS Code, tape le début d'un type d'objet dans un fichier .yaml (pod, deploy, svc, cm, ingress…) puis Tab : le squelette complet s'insère, et Tab te fait sauter d'un champ à remplir au suivant. Ctrl+Espace propose à tout moment les champs valides à l'endroit où tu es, parce que l'éditeur connaît le schéma Kubernetes.

C'est, de très loin, ce qui se fait le plus en entreprise : le dépôt contient déjà quarante manifestes qui marchent. On copie le plus proche du besoin, on renomme, on adapte. Ce n'est pas de la triche - à condition de comprendre chaque ligne qu'on garde, ce qui est exactement l'objet de ce module.

② Nettoyer ce que le générateur a ajouté

Un squelette généré contient du bruit : des rubriques vides et des rubriques que le cluster remplit lui-même. Voici la sortie brute de la commande ci-dessus, telle quelle :

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: web
  name: web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  strategy: {}              # (1)!
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - image: nginx:1.27-alpine
          name: nginx
          resources: {}     # (2)!
status: {}                  # (3)!
  1. Vide ({} = « rien dedans ») : à supprimer. Sans stratégie déclarée, Kubernetes applique la sienne par défaut - c'est très bien.
  2. Vide aussi : à supprimer, ou à remplir sciemment (requests/limits, module 4).
  3. status, c'est l'état réel, écrit par le cluster : il n'a rien à faire dans un fichier que tu écris. À supprimer, toujours.

La règle : un manifeste ne décrit que ce que tu veux ; tout le reste, Kubernetes le remplit. Un bon manifeste est court - et tu dois pouvoir expliquer chacune de ses lignes.

Le champ mystère creationTimestamp: null

Tu le croiseras dans des exemples en ligne et dans les sorties des kubectl plus anciens que le tien : c'est le même bruit, à supprimer de la même façon.

③ Compléter, avec le dictionnaire ouvert

Deux aides, sans jamais quitter ta sandbox :

kubectl explain deployment.spec                      # tous les champs possibles, expliqués
kubectl explain deployment.spec.template.spec.containers
kubectl api-resources                                # tous les types d'objets + leur apiVersion

Et dans l'éditeur : Ctrl+Espace pour la liste des champs valides, le survol pour lire la doc d'un champ, et les erreurs affichées en bout de ligne dès que tu écris quelque chose que le schéma refuse.

kubectl explain répond aussi à « c'est quoi cet objet ? »

kubectl explain deployment sans suffixe donne la description de l'objet lui-même. C'est la doc officielle, à la version exacte de ton cluster - plus fiable qu'un article de blog écrit trois versions plus tôt.

④ Vérifier avant de subir

Trois filets, du moins cher au plus cher :

kubectl apply -f web.yaml --dry-run=server   # l'API VALIDE pour de vrai, sans rien créer
kubectl diff -f web.yaml                     # ce que ça CHANGERAIT sur le cluster actuel
kubectl apply -f web.yaml                    # on y va
kubectl get -f web.yaml                      # et on vérifie le résultat
--dry-run=client --dry-run=server
Qui juge kubectl, sur ton poste l'API server du cluster
Détecte le YAML mal formé en plus : champ inconnu, quota dépassé, refus d'admission
Crée quelque chose ? non non plus

kubectl diff est le réflexe qui sauve en production : il montre exactement ce que ton apply va modifier, avant de le faire.

La boucle à graver

Squelette → nettoie → complète → vérifie → applique → contrôle. Tant que cette boucle tourne, écrire du YAML n'a rien d'un exercice de mémoire.

Et kubectl edit, alors ?

L'édition à chaud (module 3) écrit déjà du YAML… mais directement dans le cluster : pas de fichier, pas d'historique, et ton correctif disparaît au prochain apply de l'ancien manifeste. edit dépanne, le fichier livre - les deux gestes ouvrent le même YAML, seule la destination change.

De 8 lignes à une vraie application

Le manifeste ne devient jamais « plus compliqué » : il empile les mêmes briques. On monte l'escalier marche par marche.

Marche 1 - le Pod nu (ce qu'on ne livre pas)

Le Pod du début de module marche… et ne survit à rien : si son nœud tombe, personne ne le recrée (module 1). On ne pilote pas un Pod directement, on pilote un Deployment.

Marche 2 - le Deployment : un manifeste dans un manifeste

C'est le point qui bloque tout le monde, et il n'y a qu'une chose à comprendre :

spec.template, c'est un Pod

Le template d'un Deployment est un manifeste de Pod complet, moins ses deux premières lignes (apiVersion et kind - inutiles, on sait déjà que ce sera un Pod). C'est le moule : le Deployment fabrique autant de pods identiques que demandé.

apiVersion: apps/v1          # (1)!
kind: Deployment
metadata:
  name: web
spec:                        # le spec du DEPLOYMENT
  replicas: 3                # (2)!
  selector:                  # (3)!
    matchLabels:
      app: web
  template:                  # (4)!
    metadata:
      labels:
        app: web             # (5)!
    spec:                    # le spec du POD
      containers:
        - name: web
          image: nginx:1.27-alpine
  1. apps/v1, et non v1 : un Deployment vit dans la famille apps. Se tromper ici donne no matches for kind "Deployment" in version "v1" - le message le plus fréquent du module.
  2. Combien de copies je veux. Le contrôleur s'occupe du reste, en permanence.
  3. Quels pods ce Deployment considère comme les siens. Il les reconnaît à leur étiquette, pas à leur nom.
  4. Le moule : à partir d'ici et jusqu'à la fin, c'est un manifeste de Pod.
  5. L'étiquette collée sur chaque pod fabriqué. Elle doit être identique à celle du selector juste au-dessus, sinon l'API refuse le manifeste.

Le spec d'un Deployment répond donc à trois questions : combien (replicas), qui est à moi (selector), à quoi ils ressemblent (template).

Marche 3 - le Service : tout tient à une étiquette

Le Service ne connaît pas le Deployment. Il ne connaît que des labels. On peut écrire les deux objets dans un même fichier, séparés par une ligne --- :

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:                  # (1)!
    app: web
  ports:
    - port: 80               # (2)!
  1. Le Service adresse tous les pods qui portent ce label, d'où qu'ils viennent.
  2. Le port sur lequel le Service écoute. (Le port du conteneur, targetPort, est identique par défaut - les variantes sont au module 4r.)
graph LR
    SEL[Deployment<br/>selector.matchLabels<br/>app: web] -. doivent être identiques .- TPL[template.labels<br/>app: web]
    SVC[Service<br/>selector: app: web] -->|sélectionne| TPL

Trois flèches, une seule étiquette. Le Deployment reconnaît ses pods par app: web, le Service les trouve par app: web. Une majuscule de trop dans l'un des trois, et le manifeste est refusé (Deployment) ou le Service ne route nulle part, sans le moindre message d'erreur (Service). C'est la panne réseau n°1 - et celle que tu répareras toi-même dans les TP de debug.

Plusieurs objets dans un fichier

Une ligne --- seule sépare deux documents YAML. Un fichier app.yaml qui contient le Deployment et son Service s'applique d'un seul kubectl apply -f app.yaml : les objets qui vivent ensemble se livrent ensemble.

Les pièges qui mangent du temps

  1. L'indentation : 2 espaces par niveau, jamais de tabulation. Apprends à la lire (chaque niveau = un objet imbriqué) plutôt qu'à la deviner.
  2. selectorlabels : dans un Deployment, spec.selector.matchLabels doit correspondre à spec.template.metadata.labels ; dans un Service, spec.selector doit correspondre aux labels des pods visés. Sinon : rejet à l'apply, ou service qui ne route nulle part.
  3. apiVersion erronée : un Deployment est dans apps/v1, pas v1. Le message (no matches for kind …) le dit - encore faut-il le lire. En cas de doute : kubectl api-resources | grep -i deployment.
  4. L'unité des ressources : memory: 64Mi (pas 64M), cpu: 250m = 0,25 CPU, et requestslimits (sinon le pod est refusé - c'est un TP de debug).
  5. Le nom de l'objet : minuscules, chiffres et tirets uniquement (norme DNS). Pas d'accent, pas d'espace, pas de majuscule - mon-pod passe, Mon Pod non.
  6. Le --- oublié : deux objets collés dans un fichier sans séparateur, et kubectl ne voit qu'un manifeste bancal.

Lire un manifeste que tu n'as pas écrit

C'est ce que tu feras le plus souvent : en mission, on hérite du YAML des autres bien plus qu'on n'en crée. Quatre questions, dans l'ordre, et tu as compris n'importe quel fichier :

  1. kind + metadata.name : c'est quoi, et ça s'appelle comment ?
  2. spec : combien de copies, quelle image, quels ports ?
  3. Les labels : qui va retrouver cet objet, et par quelle étiquette ?
  4. Ce que je ne comprends paskubectl explain <kind>.<champ>, tout de suite.

Et pour comparer le fichier au réel : kubectl get <kind> <nom> -o yaml affiche l'objet tel que le cluster le voit, status compris.

Exercice 3y.1 - Lis-moi ce manifeste

Un objet tourne déjà dans ta sandbox et tu n'as pas écrit son manifeste. Va le chercher et réponds aux quatre questions à voix haute, dans l'ordre, sans rien modifier :

kubectl get deploy -A                          # choisis-en un que tu ne connais pas
kubectl get deploy <nom> -n <namespace> -o yaml
  1. Quel kind, quel nom ? 2. Que demande son spec (replicas, image, ports) ?
  2. Quels labels porte-t-il, et qui s'en sert ? 4. Un champ que tu ne reconnais pas → kubectl explain deployment.<le champ>.

Puis repère la frontière : à partir d'où commence le status - donc ce que tu n'aurais pas écrit si tu avais produit ce fichier toi-même ?

Gestes vus en séance

L'injection d'un ConfigMap dans un pod (envFrom) se montre en séance - le TP yaml-3-config te la fera écrire, et la cheat-sheet kubectl en garde le squelette.

Pratique

Dans ta sandbox (VS Code conseillée pour écrire ; sinon nano), une fois la lecture faite - tu écris tes manifestes :

tp start yaml-1-pod              # générer un squelette, le comprendre, l'adapter
tp start yaml-2-deploy-service   # un Deployment + un Service (lien par les labels)
tp start yaml-3-config           # un ConfigMap + l'injecter dans un pod

Puis le geste le plus réaliste du module : on te donne un YAML qui ne marche pas, à toi de le diagnostiquer et de le corriger. Un piège par TP, du message d'erreur franc au bug silencieux :

tp start yaml-debug-1   # apiVersion/selector : l'apply refuse - lire l'erreur
tp start yaml-debug-2   # service sans endpoint : tout est « vert », rien ne répond
tp start yaml-debug-4   # requests > limits : le pod est rejeté

(Un quatrième manifeste cassé - une probe fausse - t'attend au module 4c, là où les probes s'enseignent.)

La méthode de réparation est toujours la même : lis le message (apply ou Events), compare le manifeste aux rubriques que tu connais, corrige, réapplique, vérifie.

fais la manip puis tp check (ou tp watch) · tp hint / tp solution si tu bloques.

Ce qu'il faut retenir

  • Impératif pour explorer, déclaratif (YAML + apply -f) pour livrer.
  • YAML = clé/valeur, imbrication par l'indentation, listes à tirets. Rien d'autre.
  • Les rubriques de tout objet : apiVersion / kind / metadata / spec (+ status, que le cluster écrit).
  • Personne n'écrit de zéro : squelette (--dry-run=client -o yaml, modèle de l'éditeur, manifeste voisin) → nettoie → complète (kubectl explain) → vérifie → applique.
  • spec.template d'un Deployment = un manifeste de Pod ; c'est le moule.
  • Le lien Deployment ↔ Service ↔ Pods passe par les labels : c'est là que ça casse.
  • Un YAML cassé se lit : message d'apply d'abord, Events ensuite.

Quiz éclair

Teste-toi : réponds de tête, puis déplie.

1. kubectl apply -f sur un objet qui existe déjà : que se passe-t-il ?

L'objet est mis à jour pour correspondre au manifeste (apply est idempotent). C'est toute la force du déclaratif : le même fichier crée ou corrige.

2. Dans un Deployment, à quoi correspond spec.template ?

À un manifeste de Pod complet, moins apiVersion et kind : c'est le moule à partir duquel le Deployment fabrique ses pods. D'où l'imbrication de deux spec dans le même fichier.

3. Ton Service est créé sans erreur mais ne route vers rien. Premier réflexe ?

kubectl get endpoints <svc> : s'il est vide, le selector du Service ne matche pas les labels des pods. Compare-les champ à champ.

4. Comment générer un squelette de manifeste sans tout taper ?

kubectl create … --dry-run=client -o yaml > fichier.yaml - rien n'est créé sur le cluster, mais le YAML est complet et valide. Puis on nettoie (status: {}, les rubriques vides) avant de l'adapter.

5. --dry-run=client et --dry-run=server : la différence ?

client : kubectl valide seul, sur ton poste - il attrape le YAML mal formé. server : le manifeste part à l'API server, qui le valide pour de vrai (champ inconnu, quota, admission) sans rien créer. C'est le filet à passer avant un apply en production - avec kubectl diff -f, qui montre ce qui changerait.

➡️ Suite : Module 4 - Gouvernance du cluster