<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Zwindler's Reflection</title><link>https://blog.zwindler.fr/</link><description>Recent content on Zwindler's Reflection</description><generator>Hugo -- gohugo.io</generator><language>fr</language><copyright>Licensed under CC BY-SA 4.0</copyright><lastBuildDate>Sun, 29 Mar 2026 18:00:00 +0200</lastBuildDate><atom:link href="https://blog.zwindler.fr/index.xml" rel="self" type="application/rss+xml"/><item><title>101 façons de déployer Kubernetes : 125 solutions, des PRs communautaires et une UI qui s'améliore</title><link>https://blog.zwindler.fr/2026/03/29/101-facons-de-deployer-kubernetes-v3/</link><pubDate>Sun, 29 Mar 2026 18:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2026/03/29/101-facons-de-deployer-kubernetes-v3/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/03/125-kube.webp" alt="Featured image of post 101 façons de déployer Kubernetes : 125 solutions, des PRs communautaires et une UI qui s'améliore" /&gt;&lt;h2 id="il-bouge-encore"&gt;Il bouge encore
&lt;/h2&gt;&lt;p&gt;Depuis la sortie de &lt;a class="link" href="https://blog.zwindler.fr/2026/02/09/101-facons-de-deployer-kubernetes-nouvelle-ui/" &gt;l&amp;rsquo;interface web du projet&lt;/a&gt; début février, le projet a continué à évoluer. Ajout de solutions manquantes, petites améliorations de l&amp;rsquo;UI, contributions externes, optimisations de performances&amp;hellip; Bref, un bon petit mois bien rempli pour ce gros side project que j&amp;rsquo;aimerais faire grossir.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://zwindler.github.io/101-ways-to-deploy-kubernetes/" target="_blank" rel="noopener"
&gt;101-ways-to-deploy-kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note : si vous n&amp;rsquo;avez pas suivi les épisodes précédents :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2025/11/02/93-facons-de-deployer-kubernetes/" &gt;93 façons de déployer Kubernetes&lt;/a&gt; — le Google Sheet original&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/01/04/101-facons-de-deployer-kubernetes-v2/" &gt;101 façons de déployer Kubernetes (v2)&lt;/a&gt; — passage sur GitHub&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/02/09/101-facons-de-deployer-kubernetes-nouvelle-ui/" &gt;Une nouvelle UI pour explorer les 118+ solutions&lt;/a&gt; — l&amp;rsquo;appli Astro + Tailwind&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="7-nouvelles-solutions"&gt;7 nouvelles solutions
&lt;/h2&gt;&lt;p&gt;Le catalogue est passé de 118 à &lt;strong&gt;125 solutions&lt;/strong&gt;. Voici les ajouts depuis le dernier article :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Xen Orchestra&lt;/strong&gt; - ajouté pendant un stream de Cuistops =&amp;gt; Déploiement de clusters Kubernetes (MicroK8s) via les &amp;ldquo;recettes&amp;rdquo; de Xen Orchestra, la plateforme de management de Vates. Catégorie ManagementPlatform.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Charmed Kubernetes&lt;/strong&gt; - suite à un commentaire sur LinkedIn, j&amp;rsquo;ai remplacé l&amp;rsquo;entrée &amp;ldquo;Juju&amp;rdquo; par Charmed Kubernetes, qui est le vrai nom du produit Canonical pour déployer Kubernetes via Juju.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;kubeaver&lt;/strong&gt; - découvert en scrollant bluesky sans but =&amp;gt; un installateur Kubernetes offline/online avec une GUI, basé sur Kubespray et Ansible. Certifié CNCF conformance (pour ce que ça vaut&amp;hellip;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;talos-bootstrap&lt;/strong&gt; - script interactif de Cozystack pour bootstrapper des clusters Kubernetes sur Talos OS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;boot-to-talos&lt;/strong&gt; - Outil de Cozystack (encore) qui convertit n&amp;rsquo;importe quel OS existant en Talos Linux, complètement depuis le userspace. Partiellement utilisé par Quentin dans un contexte que je ne peux révéler ;-)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TOPF&lt;/strong&gt; - Talos Orchestrator by PostFinance, un CLI pour gérer des clusters Kubernetes basés sur Talos. Découvert sur le discord de Cuistops&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Talhelper&lt;/strong&gt; - Outil CLI pour créer des clusters Talos en mode GitOps, à partir d&amp;rsquo;un seul fichier YAML déclaratif. Je l&amp;rsquo;avais initialement mis de côté mais à y réfléchir, pourquoi pas le mettre.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On notera qu&amp;rsquo;au même titre que kubeadm et k3s, il y a beaucoup de solutions qui s&amp;rsquo;appuient sur &lt;strong&gt;Talos Linux&lt;/strong&gt;. De mon point de vue, ça valide l&amp;rsquo;OS et ça confirme l&amp;rsquo;adoption croissante de cet OS immutable pour Kubernetes.&lt;/p&gt;
&lt;h2 id="refonte-complète-des-tags"&gt;Refonte complète des tags
&lt;/h2&gt;&lt;p&gt;Un gros chantier a été la &lt;strong&gt;refonte de tous les tags&lt;/strong&gt; du catalogue. Ils étaient incohérents : des noms d&amp;rsquo;outils utilisés comme tags (openshift, rancher&amp;hellip;), des tags redondants (on-premise + self-hosted), des tags manquants ou parfois faux (par ex. &amp;ldquo;lightweight&amp;rdquo; sur des outils qui lancent des VMs).&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai nettoyé les tags de &lt;strong&gt;toutes les catégories&lt;/strong&gt; :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Desktop&lt;/strong&gt; : suppression des noms d&amp;rsquo;outils comme tags, ajout de &lt;code&gt;gui&lt;/code&gt; pour Docker Desktop, Orbstack, Podman Desktop, Rancher Desktop&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Selfhosted&lt;/strong&gt; : suppression du tag redondant &lt;code&gt;on-premise&lt;/code&gt; (29 solutions), ajout de tags pertinents (&lt;code&gt;automation&lt;/code&gt;, &lt;code&gt;edge&lt;/code&gt;, &lt;code&gt;lightweight&lt;/code&gt;, &lt;code&gt;bare-metal&lt;/code&gt;&amp;hellip;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ManagementPlatform&lt;/strong&gt; : toutes les solutions ont maintenant 3 tags (&lt;code&gt;multi-cloud&lt;/code&gt;, &lt;code&gt;managed&lt;/code&gt;, &lt;code&gt;gui&lt;/code&gt;, &lt;code&gt;automation&lt;/code&gt;&amp;hellip;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KubernetesInKubernetes&lt;/strong&gt; : correction de vcluster (&lt;code&gt;virtualization&lt;/code&gt; → &lt;code&gt;lightweight&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="de-nouveaux-filtres-dans-lui"&gt;De nouveaux filtres dans l&amp;rsquo;UI
&lt;/h2&gt;&lt;p&gt;J&amp;rsquo;ai ajouté un &lt;strong&gt;filtre par nombre d&amp;rsquo;étoiles GitHub&lt;/strong&gt; qui n&amp;rsquo;apparaît que quand le filtre &amp;ldquo;Open Source only&amp;rdquo; est activé. Plutôt pratique pour identifier rapidement les projets les plus populaires (&amp;gt; 100, &amp;gt; 1k, &amp;gt; 10k stars).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/03/stars-filter.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;L&amp;rsquo;occasion aussi de réorganiser la barre de filtres : le sélecteur de statut (Active/Abandoned) est passé en dropdown compact, et le layout général a été retravaillé pour rester lisible même avec plus de filtres.&lt;/p&gt;
&lt;p&gt;Dernier point, lorsque vous faites une recherche dans la barre principale, une variable vient d&amp;rsquo;ajouter à l&amp;rsquo;URL : pratique pour partager des solutions directement avec le lien.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/03/search.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="les-contributions-dantoine-caron-slashgear"&gt;Les contributions d&amp;rsquo;Antoine Caron (Slashgear)
&lt;/h2&gt;&lt;p&gt;Un grand merci à &lt;a class="link" href="https://bsky.app/profile/slashgear.dev" target="_blank" rel="noopener"
&gt;Antoine Caron (@slashgear)&lt;/a&gt; qui, au-delà de m&amp;rsquo;avoir aidé sur &lt;a class="link" href="https://blog.zwindler.fr/2026/02/19/optimisation-webperf-avif-precompression/" &gt;les performances&lt;/a&gt; et &lt;a class="link" href="https://blog.zwindler.fr/2026/02/20/securite-headers-http-observatory-hugo/" &gt;la sécurité&lt;/a&gt; de ce blog, a aussi contribué directement au projet 101 ways avec plusieurs PRs !&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hiérarchie des headings&lt;/strong&gt; - les cartes utilisaient des &lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt; sans &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; dans la page. Antoine a corrigé ça pour une hiérarchie correcte (&lt;code&gt;h1&lt;/code&gt; → &lt;code&gt;h2&lt;/code&gt;), ce qui améliore la navigation pour les lecteurs d&amp;rsquo;écran.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accessibilité du champ de recherche&lt;/strong&gt; - ajout d&amp;rsquo;un vrai &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; (en &lt;code&gt;sr-only&lt;/code&gt;) et passage du &lt;code&gt;type=&amp;quot;text&amp;quot;&lt;/code&gt; à &lt;code&gt;type=&amp;quot;search&amp;quot;&lt;/code&gt; pour une meilleure sémantique.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skip links&lt;/strong&gt; - ajout de liens &amp;ldquo;skip to content&amp;rdquo; pour la navigation au clavier (WCAG 2.1, critère 2.4.1). Trois liens : vers la recherche, les filtres, et le contenu principal.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="amélioration-des-performances"&gt;Amélioration des performances
&lt;/h2&gt;&lt;p&gt;En parlant de performances, le site s&amp;rsquo;est aussi amélioré côté PageSpeed. Voici un avant/après sur Lighthouse mobile :&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/03/101-avant.avif"
loading="lazy"
alt="Score PageSpeed avant optimisation : 82"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/03/101-apr%c3%a8s.avif"
loading="lazy"
alt="Score PageSpeed après optimisation : 99"
&gt;&lt;/p&gt;
&lt;p&gt;De &lt;strong&gt;82 à 99&lt;/strong&gt; en performances mobiles, avec du 100/100 en bonnes pratiques et SEO.&lt;/p&gt;
&lt;p&gt;Ces gains viennent d&amp;rsquo;un ensemble de corrections, notamment la résolution du &lt;strong&gt;clipping des cartes au hover&lt;/strong&gt;, un bug CSS assez vicieux lié à &lt;code&gt;content-visibility: auto&lt;/code&gt; qui impliquait &lt;code&gt;contain: paint&lt;/code&gt; et coupait le haut des cartes quand elles se soulevaient au survol. Antoine avait identifié le problème avec l&amp;rsquo;&lt;code&gt;overflow-hidden&lt;/code&gt;, et Copilot a trouvé la cause racine dans le CSS containment.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/03/clipping.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="merci-"&gt;Merci !
&lt;/h2&gt;&lt;p&gt;Un grand merci à tous ceux qui contribuent, que ce soit en me mentionnant les outils manquants, en soumettant des PRs, ou simplement en partageant le projet.&lt;/p&gt;
&lt;p&gt;Merci spécial à &lt;strong&gt;Antoine Caron&lt;/strong&gt; pour ses PRs sur l&amp;rsquo;accessibilité et la CI - c&amp;rsquo;est vraiment chouette de voir quelqu&amp;rsquo;un prendre le temps d&amp;rsquo;améliorer un projet open source qui n&amp;rsquo;est pas le sien. Et c&amp;rsquo;est d&amp;rsquo;autant plus appréciable qu&amp;rsquo;Antoine m&amp;rsquo;avait déjà aidé sur ce blog avec &lt;a class="link" href="https://blog.zwindler.fr/2026/02/19/optimisation-webperf-avif-precompression/" &gt;l&amp;rsquo;optimisation des images en AVIF&lt;/a&gt; et &lt;a class="link" href="https://blog.zwindler.fr/2026/02/20/securite-headers-http-observatory-hugo/" &gt;les security headers HTTP&lt;/a&gt;. 🙏&lt;/p&gt;
&lt;p&gt;Le projet compte maintenant &lt;strong&gt;125 solutions&lt;/strong&gt;. Si vous en connaissez d&amp;rsquo;autres, n&amp;rsquo;hésitez pas à &lt;a class="link" href="https://github.com/zwindler/101-ways-to-deploy-kubernetes/issues" target="_blank" rel="noopener"
&gt;ouvrir une issue&lt;/a&gt; ou une PR (ou juste à me ping, en cas de flemme) !&lt;/p&gt;
&lt;p&gt;👉 &lt;a class="link" href="https://zwindler.github.io/101-ways-to-deploy-kubernetes/" target="_blank" rel="noopener"
&gt;zwindler.github.io/101-ways-to-deploy-kubernetes&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Décommission de kibana.zwindler.fr, le Kibana qui hébergeait les sondages Okiwi</title><link>https://blog.zwindler.fr/2026/03/25/decomission-kibana-zwindler-fr-sondages-okiwi/</link><pubDate>Wed, 25 Mar 2026 12:00:00 +0100</pubDate><guid>https://blog.zwindler.fr/2026/03/25/decomission-kibana-zwindler-fr-sondages-okiwi/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/03/okiwi-2024-2025.webp" alt="Featured image of post Décommission de kibana.zwindler.fr, le Kibana qui hébergeait les sondages Okiwi" /&gt;&lt;h2 id="la-fin-dune-époque-"&gt;La fin d&amp;rsquo;une époque ?
&lt;/h2&gt;&lt;p&gt;Si vous me suivez depuis quelques années, vous savez que parmis mes nombreux side projects, j&amp;rsquo;ai beaucoup participé à des sondages sur les salaires dans la tech Girondine, organisés par l&amp;rsquo;association &lt;a class="link" href="https://okiwi.org/" target="_blank" rel="noopener"
&gt;Okiwi&lt;/a&gt;. C&amp;rsquo;est un projet qui a contribué à me rendre un peu &amp;ldquo;visible&amp;rdquo; (toute proportion gardée) dans l&amp;rsquo;écosystème local et qui m&amp;rsquo;a par ailleurs apporté beaucoup de statisfaction, car j&amp;rsquo;ai vraiment à coeur les sujets autour des salaires ou de la transparence salariale.&lt;/p&gt;
&lt;p&gt;Au delà de la participation à la création et la diffusion du sondage, j&amp;rsquo;hébergeais un serveur Kibana pour permettre à tout le monde de consulter les résultats de manière interactive (et je dirais même ludique).&lt;/p&gt;
&lt;p&gt;Ce serveur, &lt;code&gt;kibana.zwindler.fr&lt;/code&gt;, a hébergé donc entre 2019 et 2025 pas moins de 6 éditions du sondage, plus une collaboration avec &lt;a class="link" href="https://blog.zwindler.fr/2020/07/10/les-salaires-de-lit-dans-un-kibana/" &gt;Shirley Almosni Chiche&lt;/a&gt; sur les salaires de l&amp;rsquo;IT à l&amp;rsquo;échelle nationale.&lt;/p&gt;
&lt;p&gt;Et bien&amp;hellip; c&amp;rsquo;est fini. Le serveur est décommissionné.&lt;/p&gt;
&lt;h2 id="pourquoi-"&gt;Pourquoi ?
&lt;/h2&gt;&lt;p&gt;Oui mais voilà, après une chouette campagne 2023-2024 avec 449 réponses (ce qui est énorme à l&amp;rsquo;échelle locale), le projet s&amp;rsquo;est essouflé en 2025 avec très peu de réponses (autour de 160, bien en deça des années précédentes). Le sondage 2025-2026 n&amp;rsquo;a du coup jamais été lancé. Ce gros side projet (j&amp;rsquo;y ai passé un paquet d&amp;rsquo;heures) est terminé.&lt;/p&gt;
&lt;p&gt;Et faute de besoin, je ne maintenais donc plus ce serveur depuis bien longtemps. Il y a une semaine, en me connectant dessus pour la première fois depuis des mois, j&amp;rsquo;ai constaté qu&amp;rsquo;Elasticsearch et Kibana étaient down. Le serveur tournait, mais les services ne répondaient plus.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Personne ne s&amp;rsquo;en est plaint&lt;/strong&gt; (moi inclus, d&amp;rsquo;ailleurs).&lt;/p&gt;
&lt;p&gt;Ca faisait plus d&amp;rsquo;un mois que les dashboards étaient inaccessibles et je ne m&amp;rsquo;en étais même pas rendu compte. Si le mec qui héberge le truc ne remarque pas que c&amp;rsquo;est cassé, c&amp;rsquo;est probablement le signe qu&amp;rsquo;il est temps de tirer la prise.&lt;/p&gt;
&lt;p&gt;Le serveur (un VPS Ubuntu chez nua.ge) accumulait les mises à jour non faites, tournait sur une version d&amp;rsquo;Elastic qui n&amp;rsquo;était plus à jour, et consommait des ressources (et de l&amp;rsquo;argent) pour un service que plus personne n&amp;rsquo;utilisait.&lt;/p&gt;
&lt;h2 id="ce-que-jai-sauvegardé"&gt;Ce que j&amp;rsquo;ai sauvegardé
&lt;/h2&gt;&lt;p&gt;Avant de tout supprimer, j&amp;rsquo;ai quand même pris le temps de sauvegarder l&amp;rsquo;intégralité des données et de faire des captures d&amp;rsquo;écran de chaque dashboard (c&amp;rsquo;est mon côté hoarder de la data). Parce que même si les dashboards interactifs n&amp;rsquo;existent plus, les données restent potentiellement intéressantes.&lt;/p&gt;
&lt;h2 id="captures-décran-des-dashboards"&gt;Captures d&amp;rsquo;écran des dashboards
&lt;/h2&gt;&lt;p&gt;Voici une capture de chaque édition du sondage Okiwi telle qu&amp;rsquo;elle apparaissait dans Kibana :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/okiwi-2024-2025.avif" &gt;Dashboard Okiwi 2024-2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/okiwi-2023_2024.avif" &gt;Dashboard Okiwi 2023-2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/okiwi-2022-2023.avif" &gt;Dashboard Okiwi 2022-2023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/okiwi-2021-2022.avif" &gt;Dashboard Okiwi 2021-2022&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/okiwi-2020-2021.avif" &gt;Dashboard Okiwi 2020-2021&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/okiwi-2019-2020.avif" &gt;Dashboard Okiwi 2019-2020&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="étude-shirley-almosni-chiche-2020-national"&gt;Étude Shirley Almosni Chiche (2020, national)
&lt;/h3&gt;&lt;p&gt;En 2020, &lt;a class="link" href="https://blog.zwindler.fr/2020/07/10/les-salaires-de-lit-dans-un-kibana/" &gt;Shirley Almosni Chiche&lt;/a&gt; (agent de carrière IT) avait lancé de son côté une étude sur les salaires de l&amp;rsquo;IT à l&amp;rsquo;échelle nationale. Elle avait récolté pas mal de réponses (plus de 400) et comme j&amp;rsquo;avais déjà l&amp;rsquo;infrastructure Kibana qui tournait pour Okiwi, je lui ai proposé d&amp;rsquo;héberger ses données sur mon serveur pour que ce soit plus ludique d&amp;rsquo;explorer les résultats qu&amp;rsquo;un simple Google Sheet.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/shirley-2020.avif" &gt;Dashboard Shirley - Salaires IT national 2020&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="offres-demploi-de-recruteurs-2019-2025-données-personnelles"&gt;Offres d&amp;rsquo;emploi de recruteurs (2019-2025, données personnelles)
&lt;/h3&gt;&lt;p&gt;Celui-là n&amp;rsquo;a jamais été publié, mais tant qu&amp;rsquo;à sauvegarder, autant tout garder. Depuis 2019, je complète un Google Sheet avec toutes les offres d&amp;rsquo;emploi que m&amp;rsquo;envoient les recruteurs : fourchettes de salaires quand elles sont disponibles, type d&amp;rsquo;entreprise, remote ou non, étranger ou pas (et le pays concerné le cas échéant). J&amp;rsquo;avais importé tout ça dans Kibana pour avoir un dashboard interactif.&lt;/p&gt;
&lt;p&gt;C&amp;rsquo;est subjectif (c&amp;rsquo;est uniquement ce que &lt;em&gt;je&lt;/em&gt; reçois), mais ça me permet d&amp;rsquo;avoir mon propre point de vue sur le marché du travail dans l&amp;rsquo;IT et sur les pratiques de chasse des recruteurs. Je trouve ça intéressant comme données, même si c&amp;rsquo;est à prendre avec des pincettes (c&amp;rsquo;est le moins qu&amp;rsquo;on puisse dire).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/joboffers.avif" &gt;Dashboard offres d&amp;rsquo;emploi recruteurs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="exports-json-des-données-brutes"&gt;Exports JSON des données brutes
&lt;/h2&gt;&lt;p&gt;Pour ceux qui voudraient retravailler les données (ou les importer dans un autre outil), j&amp;rsquo;ai exporté l&amp;rsquo;intégralité des index Elasticsearch au format JSON. Les noms d&amp;rsquo;index ne sont pas toujours très parlants (merci moi du passé), donc voici la correspondance :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Edition&lt;/th&gt;
&lt;th&gt;Index Elasticsearch&lt;/th&gt;
&lt;th&gt;Téléchargement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2019-2020&lt;/td&gt;
&lt;td&gt;&lt;code&gt;import-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/import-1.json" &gt;import-1.json&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2020-2021&lt;/td&gt;
&lt;td&gt;&lt;code&gt;okiwi21&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/okiwi21.json" &gt;okiwi21.json&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2021-2022&lt;/td&gt;
&lt;td&gt;&lt;code&gt;okiwi-2022&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/okiwi-2022.json" &gt;okiwi-2022.json&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022-2023&lt;/td&gt;
&lt;td&gt;&lt;code&gt;okiwi-2023&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/okiwi-2023.json" &gt;okiwi-2023.json&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-2024&lt;/td&gt;
&lt;td&gt;&lt;code&gt;okiwi-2024-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/okiwi-2024-2.json" &gt;okiwi-2024-2.json&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-2025&lt;/td&gt;
&lt;td&gt;&lt;code&gt;okiwi-2025&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/okiwi-2025.json" &gt;okiwi-2025.json&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Étude Shirley (national)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;shirley&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/03/shirley.json" &gt;shirley.json&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="rétrospective"&gt;Rétrospective
&lt;/h2&gt;&lt;p&gt;Ce Kibana aura quand même eu une belle vie. Lancé un peu sur un coup de tête en 2019 pour rendre les résultats du premier sondage Okiwi plus sexy qu&amp;rsquo;un Google Sheet, il a fini par devenir un rendez-vous annuel.&lt;/p&gt;
&lt;p&gt;Les dashboards interactifs ont permis à des centaines de personnes de la tech Girondine de comparer leurs salaires, de repérer des tendances, et même de servir d&amp;rsquo;argument lors de négociations annuelles dans certaines entreprises. C&amp;rsquo;est pas rien.&lt;/p&gt;
&lt;p&gt;Au fil des années, j&amp;rsquo;ai appris à nettoyer des données dégueulasses issues de Google Forms (puis framaforms), à me battre avec des aggrégations Elasticsearch, à découvrir &lt;a class="link" href="https://blog.zwindler.fr/2022/01/03/visualisations-personalises-kibana-vega" &gt;Vega pour faire des nuages de points&lt;/a&gt; que Kibana ne proposait pas nativement, et à gérer un serveur Elastic en production (spoiler : c&amp;rsquo;est pas de tout repos).&lt;/p&gt;
&lt;h2 id="bientôt-plus-besoin-de-ce-genre-dinitiatives-"&gt;Bientôt plus besoin de ce genre d&amp;rsquo;initiatives ?
&lt;/h2&gt;&lt;p&gt;Petit aparté pour finir sur une note positive : ce type de sondage artisanal pourrait bientôt devenir moins nécessaire. La directive européenne 2023/970 sur la transparence des rémunérations, adoptée en 2023, impose aux entreprises de plus de 100 salariés de publier des informations sur les écarts de rémunération et donne aux candidats le droit de connaître la fourchette salariale d&amp;rsquo;un poste avant l&amp;rsquo;entretien.&lt;/p&gt;
&lt;p&gt;Les États membres ont jusqu&amp;rsquo;à juin 2026 pour la transposer dans leur droit national. La France n&amp;rsquo;a, à l&amp;rsquo;heure où j&amp;rsquo;écris ces lignes, toujours pas transposé cette directive&amp;hellip; mais on peut espérer que ça finisse par arriver. Le jour où les fourchettes de salaires seront affichées sur les offres d&amp;rsquo;emploi et où les entreprises devront rendre des comptes sur les écarts de rémunération, nos petits sondages Okiwi auront fait leur temps. Et honnêtement, ce sera une bonne nouvelle.&lt;/p&gt;
&lt;h2 id="les-articles-liés"&gt;Les articles liés
&lt;/h2&gt;&lt;p&gt;Pour ceux qui veulent retrouver le contexte de chaque édition :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2020/07/10/les-salaires-de-lit-dans-un-kibana/" &gt;Les salaires de l&amp;rsquo;IT dans un Kibana (étude Shirley, 2020)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2020/11/16/okiwi-grille-de-salaires-anonyme-dans-lit-a-bordeaux-2020-2021/" &gt;Okiwi - Grille de salaires 2020-2021&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2021/12/13/okiwi-grille-de-salaires-anonyme-dans-la-tech-girondine-2021-2022/" &gt;Okiwi - Grille de salaires 2021-2022&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2022/11/21/okiwi-sondage-des-salaires-dans-la-tech-girondine-2022-2023-analyse/" &gt;Okiwi - Sondage salaires 2022-2023 + analyse&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2023/11/12/okiwi-sondage-des-salaires-dans-la-tech-girondine-2023-2024-coup-d-envoi" &gt;Okiwi - Sondage salaires 2023-2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2022/01/03/visualisations-personalises-kibana-vega" &gt;Visualisations personnalisées Kibana avec Vega&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Have fun !&lt;/p&gt;</description></item><item><title>GenAI et développement logiciel, épisode 2 : kubectl-debug-pvc, de l'idée à krew en 2x30 minutes</title><link>https://blog.zwindler.fr/2026/03/16/genai-llm-dev-kubectl-debug-pvc/</link><pubDate>Tue, 17 Mar 2026 18:00:00 +0100</pubDate><guid>https://blog.zwindler.fr/2026/03/16/genai-llm-dev-kubectl-debug-pvc/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/03/kubectl-debug-pvc.webp" alt="Featured image of post GenAI et développement logiciel, épisode 2 : kubectl-debug-pvc, de l'idée à krew en 2x30 minutes" /&gt;&lt;h2 id="précédemment-dans-genai-et-dev"&gt;Précédemment, dans &amp;ldquo;GenAI et dev&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;Dans &lt;a class="link" href="https://blog.zwindler.fr/2026/03/08/genai-llm-dev-podsweeper/" &gt;mon article précédent&lt;/a&gt;, je vous parlais de mon retour d&amp;rsquo;expérience avec PodSweeper, un projet développé avec OpenCode et Claude Opus. Le bilan était nuancé : vitesse brute impressionnante, mais race conditions, gestion d&amp;rsquo;erreur laxiste, et nécessité constante de supervision humaine (entre autres déconvenues).&lt;/p&gt;
&lt;p&gt;Aujourd&amp;rsquo;hui, je vous parle d&amp;rsquo;un deuxième projet, bien plus simple, né d&amp;rsquo;un vrai besoin en production. Et le constat est assez différent.&lt;/p&gt;
&lt;h2 id="lincident-qui-a-tout-déclenché"&gt;L&amp;rsquo;incident qui a tout déclenché
&lt;/h2&gt;&lt;p&gt;Vous connaissez peut-être cette situation : un pod en production, en état de fonctionnement, qui utilise un PVC en &lt;code&gt;ReadWriteOnce&lt;/code&gt;. Vous avez besoin d&amp;rsquo;aller regarder le contenu de ce volume. Bonne pratique de production : le pod en question ne dispose pas de shell (on réduit la surface d&amp;rsquo;attaque). Pas de &lt;code&gt;/bin/sh&lt;/code&gt;, pas de &lt;code&gt;/bin/bash&lt;/code&gt;, rien.&lt;/p&gt;
&lt;p&gt;Pas grave, on a &lt;code&gt;kubectl debug&lt;/code&gt; pour ça, me direz-vous !&lt;/p&gt;
&lt;p&gt;Ok, créons un conteneur éphémère dans le pod et&amp;hellip; ah non. &lt;code&gt;kubectl debug&lt;/code&gt; ne permet pas de monter les volumes du pod dans le conteneur éphémère. Il n&amp;rsquo;expose tout simplement pas l&amp;rsquo;option &lt;code&gt;volumeMounts&lt;/code&gt; pour les conteneurs éphémères.&lt;/p&gt;
&lt;p&gt;On pourrait couper le pod, ce qui permet de monter le PVC RWO dans un autre pod avec un shell, mais on n&amp;rsquo;a pas envie, c&amp;rsquo;est de la prod.&lt;/p&gt;
&lt;p&gt;Mon collègue Maxime (encore lui !) me propose une méthode de contournement. La procédure manuelle, c&amp;rsquo;est :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Créer un conteneur éphémère sur le pod avec &lt;code&gt;kubectl debug&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Construire à la main un patch JSON pour ajouter des &lt;code&gt;volumeMounts&lt;/code&gt; au conteneur éphémère qu&amp;rsquo;on vient de créer&lt;/li&gt;
&lt;li&gt;Dans un autre terminal, se brancher &lt;del&gt;au cul du camion&lt;/del&gt; sur l&amp;rsquo;API de kube en direct avec &lt;code&gt;kubectl proxy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Appliquer le patch via un curl sur l&amp;rsquo;API Kubernetes&lt;/li&gt;
&lt;li&gt;Attendre que le conteneur soit prêt, s&amp;rsquo;attacher au conteneur&lt;/li&gt;
&lt;li&gt;Se dire qu&amp;rsquo;il y avait quand même plus simple comme métier que patcheur de container à coup de curl.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Pour ceux qui pensent que c&amp;rsquo;est jouable, &amp;ldquo;téma la gueule du patch&amp;rdquo; comme disent (disaient ?) les jeunes :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;curl&lt;/span&gt; &lt;span class="err"&gt;http:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:8001/api/v1/namespaces/&amp;lt;namespace&amp;gt;/pods/&amp;lt;pod&amp;gt;/ephemeralcontainers \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="err"&gt;-X&lt;/span&gt; &lt;span class="err"&gt;PATCH&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="err"&gt;-H&lt;/span&gt; &lt;span class="err"&gt;&amp;#39;Content-Type:&lt;/span&gt; &lt;span class="err"&gt;application/strategic-merge-patch+json&amp;#39;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="err"&gt;-d&lt;/span&gt; &lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;spec&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;ephemeralContainers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;debugger&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;image&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ubuntu&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/bin/sh&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;targetContainerName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;lt;target-container&amp;gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;stdin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;tty&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;volumeMounts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;lt;volume-name&amp;gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;mountPath&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/debug/&amp;lt;volume-name&amp;gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Si vous pensez que c&amp;rsquo;est error prone, vous avez raison. Et si vous pensez que c&amp;rsquo;est pénible à faire sous pression pendant un incident de prod, vous avez encore plus raison.&lt;/p&gt;
&lt;h2 id="side-note"&gt;Side note
&lt;/h2&gt;&lt;p&gt;Les plus aguerri·es d&amp;rsquo;entre vous dans les versions récentes de Kubernetes ont peut être entendu parler du &lt;a class="link" href="https://kep.k8s.io/2590" target="_blank" rel="noopener"
&gt;KEP-2590&lt;/a&gt;, qui introduit un (relativement) nouveau flag &lt;code&gt;--subresource&lt;/code&gt; de &lt;code&gt;kubectl&lt;/code&gt;. En effet, les containers éphémères créés par la commande &lt;code&gt;kubectl debug&lt;/code&gt; ne sont pas des &lt;em&gt;resources&lt;/em&gt; (un pod, un deployment, &amp;hellip;) mais des &lt;em&gt;subresources&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Jusqu&amp;rsquo;à cette version 1.33, il était littéralement impossible de réaliser des opérations sur les subresources avec &lt;code&gt;kubectl&lt;/code&gt;, seulement les resources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kubernetes v1.33: Octarine&lt;/strong&gt; apporte le support du flag &amp;ndash;subresource, &lt;strong&gt;mais seulement&lt;/strong&gt; pour 3 subresources pour l&amp;rsquo;instant &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;scale&lt;/code&gt; and &lt;code&gt;resize&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://kubernetes.io/blog/2025/04/23/kubernetes-v1-33-release/#subresource-support-in-kubectl" target="_blank" rel="noopener"
&gt;https://kubernetes.io/blog/2025/04/23/kubernetes-v1-33-release/#subresource-support-in-kubectl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://kubernetes.io/docs/reference/kubectl/conventions/#subresources" target="_blank" rel="noopener"
&gt;https://kubernetes.io/docs/reference/kubectl/conventions/#subresources&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pas de solution de ce côté-là non plus&amp;hellip;&lt;/p&gt;
&lt;h1 id="le-prompt-de-réunion"&gt;Le prompt de réunion
&lt;/h1&gt;&lt;p&gt;J&amp;rsquo;avais cette idée en tête depuis l&amp;rsquo;incident. Lundi matin, au début d&amp;rsquo;une réunion (pendant que les gens faisaient encore des blagues), j&amp;rsquo;ai lancé OpenCode avec Claude Opus et j&amp;rsquo;ai tapé un prompt qui décrivait :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Le problème : &lt;code&gt;kubectl debug&lt;/code&gt; ne monte pas les volumes PVC s&amp;rsquo;ils sont en RWO&lt;/li&gt;
&lt;li&gt;La procédure manuelle que je faisais à la main (les étapes douloureuses décrites un peu plus haut)&lt;/li&gt;
&lt;li&gt;Ce que je voulais : un outil qui automatise tout ça&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OpenCode + Opus m&amp;rsquo;ont posé 2 questions (et j&amp;rsquo;ai ajouté une consigne) :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&amp;ldquo;Quel langage ?&amp;rdquo;&lt;/em&gt; → &lt;strong&gt;Go&lt;/strong&gt; (je persiste)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&amp;ldquo;CLI seule ou TUI ?&amp;rdquo;&lt;/em&gt; → &lt;strong&gt;Les deux&lt;/strong&gt; (mode non-interactif pour le scripting, TUI pour l&amp;rsquo;usage quotidien)&lt;/li&gt;
&lt;li&gt;Et je lui ai demandé de toujours vérifier à chaque fois qu&amp;rsquo;il estime avoir fini de lancer le linter &lt;code&gt;golangci-lint&lt;/code&gt; (trauma de mon précédent test avec opencode)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Il est parti de lui-même sur l&amp;rsquo;idée d&amp;rsquo;une TUI avec &lt;a class="link" href="https://github.com/charmbracelet/bubbletea" target="_blank" rel="noopener"
&gt;Bubble Tea&lt;/a&gt;. Puis j&amp;rsquo;ai arrêté de regarder &amp;amp; j&amp;rsquo;ai suivi ma réunion.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;~30 minutes plus tard, fin de la réunion&lt;/strong&gt;, j&amp;rsquo;ai regardé le résultat. Le POC était fonctionnel.&lt;/p&gt;
&lt;h2 id="ce-quopus-a-produit-en-30-minutes"&gt;Ce qu&amp;rsquo;Opus a produit en 30 minutes
&lt;/h2&gt;&lt;p&gt;Sans aucune intervention de ma part, le LLM a scaffoldé un projet Go complet :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Structure propre&lt;/strong&gt; : &lt;code&gt;cmd/&lt;/code&gt; pour le CLI Cobra, &lt;code&gt;pkg/k8s/&lt;/code&gt; pour toute l&amp;rsquo;interaction Kubernetes, &lt;code&gt;pkg/tui/&lt;/code&gt; pour la TUI Bubble Tea&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Le coeur du sujet&lt;/strong&gt; : un appel direct à l&amp;rsquo;API Kubernetes pour patcher le subresource &lt;code&gt;ephemeralcontainers&lt;/code&gt; du pod avec un strategic merge patch incluant les &lt;code&gt;volumeMounts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mode non-interactif&lt;/strong&gt; : &lt;code&gt;-n namespace -p pod -v volume:/mount/path&lt;/code&gt;, prêt pour le scripting&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TUI complète&lt;/strong&gt; : navigation vim-style (&lt;code&gt;j&lt;/code&gt;/&lt;code&gt;k&lt;/code&gt;), filtrage fuzzy (&lt;code&gt;/&lt;/code&gt;), multi-sélection de volumes, spinner de chargement&amp;hellip;&lt;/li&gt;
&lt;li&gt;Makefile avec tout un tas de targets (vet, lint, test, build, install)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ce que je lui ai demandé d&amp;rsquo;ajouter :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Discovery intelligente&lt;/strong&gt; : au lieu de scanner tous les pods de tous les namespaces (lent sur un de mes gros cluster), l&amp;rsquo;outil liste d&amp;rsquo;abord les PVCs cluster-wide (un seul appel API) pour identifier les namespaces pertinents, PUIS les pods dans le namespace sélectionné. On réduit drastiquement le nombre d&amp;rsquo;appels à l&amp;rsquo;API server de kube et la TUI ne montre que les namespaces et pods qui ont des volumes PVC&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Héritage du &lt;code&gt;SecurityContext&lt;/code&gt;&lt;/strong&gt; : le conteneur éphémère copie le &lt;code&gt;securityContext&lt;/code&gt; du conteneur cible, ce qui lui permet de passer les PodSecurity policies (&lt;code&gt;restricted&lt;/code&gt;, &lt;code&gt;baseline&lt;/code&gt;&amp;hellip;). C&amp;rsquo;est un cas que je n&amp;rsquo;avais pas envisagé dans mon prompt initial et qui a planté immédiatement sur un cluster bien configuré.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sans ces petites améliorations le tool était déjà utilisable (sauf pour le cas où vous avez configuré des SecurityContext), mais c&amp;rsquo;est nettement plus confortable à l&amp;rsquo;usage (parce que oui, je l&amp;rsquo;utilise).&lt;/p&gt;
&lt;p&gt;Le code de la mécanique centrale (le patch de l&amp;rsquo;API) fait quelques centaines de lignes de Go propre. Il récupère le pod, construit le patch JSON avec le conteneur éphémère et ses &lt;code&gt;volumeMounts&lt;/code&gt;, l&amp;rsquo;applique via &lt;code&gt;Patch()&lt;/code&gt; sur le subresource, attend que le conteneur soit Running, puis lance &lt;code&gt;kubectl attach&lt;/code&gt;. Rien de sorcier, c&amp;rsquo;est juste ce qu&amp;rsquo;il faut, bien fait du premier coup.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-golang" data-lang="golang"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ephemeralContainer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;corev1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EphemeralContainer&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;EphemeralContainerCommon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;corev1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EphemeralContainerCommon&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;containerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/bin/sh&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Stdin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TTY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;VolumeMounts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mounts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SecurityContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;targetSecurityContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TargetContainerName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;targetContainer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Clientset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CoreV1&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Pods&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Namespace&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PodName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StrategicMergePatchType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;patchBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;metav1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PatchOptions&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ephemeralcontainers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="les-itérations-suivantes-30-minutes-ish"&gt;Les itérations suivantes (30 minutes-ish)
&lt;/h2&gt;&lt;p&gt;Une fois le POC validé, j&amp;rsquo;ai enchaîné quelques prompts ciblés. Je lui ai demandé ce qu&amp;rsquo;on pouvait améliorer. Il a proposé (et implémenté) :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;quelques corrections mineures de code qu&amp;rsquo;il avait mal codé (mais non bloquant)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Warnings&lt;/strong&gt; : si le SecurityContext hérité a &lt;code&gt;readOnlyRootFilesystem&lt;/code&gt;, &lt;code&gt;runAsNonRoot&lt;/code&gt;, ou autre mesure de sécurité, l&amp;rsquo;outil prévient l&amp;rsquo;utilisateur avant l&amp;rsquo;attach&lt;/li&gt;
&lt;li&gt;Ajouté une CI complète à base de &lt;code&gt;goreleaser&lt;/code&gt; et de github actions&lt;/li&gt;
&lt;li&gt;Ajouté de la documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="la-cerise-sur-le-macdo--publication-sur-krew"&gt;La cerise sur le Macdo : publication sur Krew
&lt;/h2&gt;&lt;p&gt;Une fois que j&amp;rsquo;avais une version propre, j&amp;rsquo;ai demandé à Opus comment faciliter l&amp;rsquo;installation du plugin. Il m&amp;rsquo;a proposé &lt;code&gt;brew&lt;/code&gt;, ou alors &lt;a class="link" href="https://krew.sigs.k8s.io/" target="_blank" rel="noopener"
&gt;Krew&lt;/a&gt;, le gestionnaire de plugins kubectl.&lt;/p&gt;
&lt;p&gt;Je lui ai demandé si le processus d&amp;rsquo;acceptation du plugin était complexe. Il a dit que non, je lui ai dit &amp;ldquo;chiche !&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Il a &lt;strong&gt;réalisé l&amp;rsquo;intégralité du processus&lt;/strong&gt; :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Création d&amp;rsquo;un fork de &lt;a class="link" href="https://github.com/kubernetes-sigs/krew-index" target="_blank" rel="noopener"
&gt;krew-index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Rédaction du manifeste Krew (le fichier &lt;code&gt;.yaml&lt;/code&gt; qui décrit le plugin)&lt;/li&gt;
&lt;li&gt;Prise en compte de toutes les bonnes pratiques demandées par les maintainers de krew-index (format des URLs de téléchargement, descriptions courtes, checksums SHA256, etc.)&lt;/li&gt;
&lt;li&gt;Préparation de la PR, directement sur krew-index&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;J&amp;rsquo;ai juste regardé. La PR a été mergée 12 heures plus tard par les mainteneurs.&lt;/p&gt;
&lt;p&gt;Résultat : &lt;code&gt;kubectl krew install debug-pvc&lt;/code&gt; fonctionne.&lt;/p&gt;
&lt;h2 id="mini-échec---déléguer-aussi-la-démo"&gt;Mini échec - déléguer aussi la démo
&lt;/h2&gt;&lt;p&gt;Fort de ce succès avec la PR pour krew-index, je me suis demandé si on ne pourrait pas faire une démo visuelle (un GIF à ajouter dans le README.md du projet qui montre comment ça fonctionne) avec le LLM.&lt;/p&gt;
&lt;p&gt;Je lui ai demandé s&amp;rsquo;il pouvait le faire avec &lt;code&gt;asciinema&lt;/code&gt; et le LLM m&amp;rsquo;a répondu que &amp;ldquo;oui&amp;rdquo; (oui, je discute avec le LLM comme avec mes collègues). Banco, on a essayé.&lt;/p&gt;
&lt;p&gt;Le résultat était &lt;em&gt;presque&lt;/em&gt; bon, j&amp;rsquo;aurais pu m&amp;rsquo;en contenter si j&amp;rsquo;étais pas quelqu&amp;rsquo;un de pénible : c&amp;rsquo;était un poil lent. J&amp;rsquo;ai l&amp;rsquo;impression la réactivité du LLM dans ses actions était inégale, ce qui rendait le rendu un peu désagréable au visionnage. J&amp;rsquo;aurais pu insister jusqu&amp;rsquo;à avoir quelque chose de correct, mais j&amp;rsquo;ai finalement enregistré moi-même une vidéo, convertie en GIF avec &lt;code&gt;ffmpeg&lt;/code&gt;. C&amp;rsquo;était plus fluide et plus rapide que d&amp;rsquo;itérer.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/03/kubectl-debug-pvc-demo.gif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="le-diff-avec-podsweeper"&gt;Le diff avec PodSweeper
&lt;/h2&gt;&lt;p&gt;La différence de ressenti entre ce projet et PodSweeper est frappante. Là où PodSweeper était un combat constant (race conditions, amnésie du LLM, fonctionnalités hors-spécifications), &lt;code&gt;kubectl-debug-pvc&lt;/code&gt; s&amp;rsquo;est déroulé sans accroc notable.&lt;/p&gt;
&lt;p&gt;Pourquoi ? Je pense que ça tient à la nature du projet :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Une seule chose à faire&lt;/strong&gt;, clairement définie : patcher un subresource Kubernetes avec des volumeMounts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pas de logique concurrente&lt;/strong&gt; : on fait une requête API, on attend, on s&amp;rsquo;attache&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Un domaine bien documenté&lt;/strong&gt; : l&amp;rsquo;API Kubernetes, Cobra, Bubble Tea. Le LLM connaît tout ça par coeur&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pas d&amp;rsquo;état partagé complexe&lt;/strong&gt; : pas de goroutines qui se marchent dessus, pas de graceful shutdown à gérer, de microservices multiples&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scope limité&lt;/strong&gt; : le projet entier tient dans une quinzaine de fichiers, CI et documentation incluses&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C&amp;rsquo;est probablement le terrain de jeu idéal pour un LLM. Un problème bien cadré, un domaine technique bien balisé, une solution linéaire.&lt;/p&gt;
&lt;h2 id="quest-ce-que-jen-pense-"&gt;Qu&amp;rsquo;est-ce que j&amp;rsquo;en pense ?
&lt;/h2&gt;&lt;p&gt;Objectivement, ce genre de projet &amp;ldquo;simple&amp;rdquo; (une chose à faire, simple à comprendre) marche &lt;strong&gt;vraiment super bien&lt;/strong&gt; avec OpenCode + Opus. Je pense que c&amp;rsquo;est ce genre à cause (grâce) de ce genre de petits projets que la hype est tellement forte autour du développement agentique. On a une idée en tête qu&amp;rsquo;on avait la flemme de dev, on teste, ça marche. &amp;ldquo;WOW&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Mais je ne veux pas non plus minimiser le travail réalisé. Le fait qu&amp;rsquo;un outil fonctionnel, propre, publié sur Krew et utilisable par n&amp;rsquo;importe qui ait pu émerger d&amp;rsquo;un prompt lancé en début de réunion, c&amp;rsquo;est quand même assez dingue.&lt;/p&gt;
&lt;p&gt;Un peu flippant aussi, de se dire qu&amp;rsquo;un nombre hallucinant de micro tools vont apparaître dans les prochains mois, avec un niveau de qualité probablement inégal.&lt;/p&gt;
&lt;h2 id="le-projet"&gt;Le projet
&lt;/h2&gt;&lt;p&gt;Le code est disponible ici :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/zwindler/kubectl-debug-pvc" target="_blank" rel="noopener"
&gt;https://github.com/zwindler/kubectl-debug-pvc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Installation :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Via Krew (recommandé)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl krew install debug-pvc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Utilisation interactive (TUI)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl debug-pvc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Utilisation non-interactive&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl debug-pvc -n my-namespace -p my-pod-0 -v data:/debug/data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Si vous aussi vous avez déjà galéré à inspecter un PVC sur un pod sans shell, essayez. Et si vous trouvez des bugs, vous pourrez blâmer le LLM 😄.&lt;/p&gt;</description></item><item><title>Flannel et NetworkPolicies : comment ajouter le support avec Cilium en CNI chaining</title><link>https://blog.zwindler.fr/2026/03/15/flannel-networkpolicies-cilium-cni-chaining/</link><pubDate>Sun, 15 Mar 2026 18:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2026/03/15/flannel-networkpolicies-cilium-cni-chaining/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/03/flannel-cilium-chaining.webp" alt="Featured image of post Flannel et NetworkPolicies : comment ajouter le support avec Cilium en CNI chaining" /&gt;&lt;h2 id="flannel-flannel-flannel"&gt;Flannel, flannel, flannel&amp;hellip;
&lt;/h2&gt;&lt;p&gt;Flannel est un CNI simple et populaire 😢.&lt;/p&gt;
&lt;p&gt;C&amp;rsquo;est le CNI par défaut de k3s, celui que la moitié des tutos kubeadm utilisent, et on le retrouve aussi dans pas mal d&amp;rsquo;offres managées.&lt;/p&gt;
&lt;p&gt;OK, il est simple, il route les paquets entre les pods, il supporte VXLAN et WireGuard, il se configure en 2 minutes. Que demander de plus ?&lt;/p&gt;
&lt;p&gt;Ben justement. Il y a un truc que flannel ne fait &lt;strong&gt;pas&lt;/strong&gt; : les NetworkPolicies. (Et c&amp;rsquo;est très grave).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Flannel is focused on networking. For network policy, other projects such as Calico can be used.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;[Edit]&lt;/strong&gt; Contrairement à ce que j&amp;rsquo;écris ici, flannel supporte en fait les NetworkPolicies depuis au moins 2 ans, via l&amp;rsquo;implémentation de référence &lt;a class="link" href="https://github.com/kubernetes-sigs/kube-network-policies" target="_blank" rel="noopener"
&gt;kube-network-policies&lt;/a&gt; du projet Kubernetes. L&amp;rsquo;option n&amp;rsquo;est pas mise en avant dans le README et ce n&amp;rsquo;est probablement pas plus recommandé en production, mais elle existe. Voir la &lt;a class="link" href="https://github.com/flannel-io/flannel/blob/master/Documentation/netpol.md" target="_blank" rel="noopener"
&gt;documentation officielle&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Et le piège, c&amp;rsquo;est que si vous n&amp;rsquo;avez pas lu cette petite ligne dans le &lt;strong&gt;README.md&lt;/strong&gt;, rien ne vous le dit explicitement.&lt;/p&gt;
&lt;p&gt;Vous pouvez parfaitement créer des objets &lt;code&gt;NetworkPolicy&lt;/code&gt; dans votre cluster, &lt;code&gt;kubectl apply&lt;/code&gt; ne bronchera pas, &lt;code&gt;kubectl get netpol&lt;/code&gt; vous les listera gentiment. Sauf que&amp;hellip; elles ne sont pas enforced. Le trafic passe quand même. Votre deny-all ne deny rien du tout.&lt;/p&gt;
&lt;h2 id="on-vérifie-pour-être-bien-sûr"&gt;On vérifie pour être bien sûr
&lt;/h2&gt;&lt;p&gt;Avant de résoudre quoi que ce soit, vérifions que le problème existe. En partant du prérequis qu&amp;rsquo;on a un cluster Kubernetes qui a flannel comme CNI et que tout est fonctionnel, on va déployer deux pods dans deux namespaces différents : un client (curl) et un serveur (nginx).&lt;/p&gt;
&lt;p&gt;On vérifie que le client peut joindre le serveur :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; -n netpol-test-a client -- &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl -s --max-time &lt;span class="m"&gt;5&lt;/span&gt; http://server.netpol-test-b.svc.cluster.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On obtient la page d&amp;rsquo;accueil nginx. Jusque-là, tout est normal. Maintenant, on applique une NetworkPolicy &lt;em&gt;deny-all&lt;/em&gt; sur le namespace du serveur :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# 01-deny-all-ingress.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;networking.k8s.io/v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;NetworkPolicy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;deny-all-ingress&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;netpol-test-b&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;podSelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;{}&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;policyTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;Ingress&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl apply -f 01-deny-all-ingress.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Et on re-teste :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; -n netpol-test-a client -- &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl -s --max-time &lt;span class="m"&gt;5&lt;/span&gt; http://server.netpol-test-b.svc.cluster.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Résultat : la page nginx s&amp;rsquo;affiche toujours.&lt;/strong&gt; La NetworkPolicy est bien créée (&lt;code&gt;kubectl get netpol -n netpol-test-b&lt;/code&gt; la montre), mais elle n&amp;rsquo;est pas enforced. Le trafic passe comme si de rien n&amp;rsquo;était.&lt;/p&gt;
&lt;p&gt;C&amp;rsquo;est le comportement attendu avec flannel (par défaut). Flannel ne fait que du routage L3 (overlay VXLAN ou WireGuard). Il n&amp;rsquo;implémente pas de contrôleur NetworkPolicy. Les objets existent dans etcd, mais personne ne les traduit en règles de filtrage.&lt;/p&gt;
&lt;p&gt;On nettoie la policy avant de passer à la suite :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl delete -f 01-deny-all-ingress.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="les-alternatives-pour-ajouter-le-support"&gt;Les alternatives pour ajouter le support
&lt;/h2&gt;&lt;p&gt;Pendant longtemps j&amp;rsquo;ai pensé que c&amp;rsquo;était une fatalité. Je pense toujours que n&amp;rsquo;est pas un bon choix pour n&amp;rsquo;importe quelle production.&lt;/p&gt;
&lt;p&gt;MAIS récemment, j&amp;rsquo;ai découvert qu&amp;rsquo;il était possible de chaîner les CNI au sein d&amp;rsquo;un même cluster, et ainsi d&amp;rsquo;avoir un CNI qui gère la majorité des tâches (ici Flannel) et un autre qui se charge d&amp;rsquo;autres tâches, comme par exemple enforcer des Netpols.&lt;/p&gt;
&lt;p&gt;C&amp;rsquo;est d&amp;rsquo;ailleurs le principe de Canal, que je connaissais de nom mais que je n&amp;rsquo;avais jamais exploré. En fait, c&amp;rsquo;est ni plus ni moins qu&amp;rsquo;un manifeste qui déploie Flannel comme CNI principal avec Calico pour l&amp;rsquo;enforcing des NetOK fine, it&amp;rsquo;s truepols !&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Canal was the name of Tigera and CoreOS’s project to integrate Calico and flannel.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Note : &lt;a class="link" href="https://github.com/projectcalico/canal?tab=readme-ov-file" target="_blank" rel="noopener"
&gt;le projet GitHub a été archivé en octobre 2025&lt;/a&gt; mais en théorie ça devrait encore fonctionner, si on trouve la doc correcte (pas trouvé, les liens sont KO, mais j&amp;rsquo;ai pas vraiment cherché non plus).&lt;/p&gt;
&lt;p&gt;Vous avez donc compris le principe, on va ajouter un composant qui va &lt;strong&gt;watch&lt;/strong&gt; les objets NetworkPolicy et les traduire en règles de filtrage effectives (iptables, eBPF, nftables&amp;hellip;), &lt;strong&gt;sans toucher au flannel existant&lt;/strong&gt;. C&amp;rsquo;est ce qu&amp;rsquo;on appelle le &amp;ldquo;CNI chaining&amp;rdquo; ou le mode &amp;ldquo;policy-only&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Il existe plusieurs solutions :&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Calico (Canal)&lt;/strong&gt;, Historiquement, la combinaison flannel + Calico s&amp;rsquo;appelle &amp;ldquo;Canal&amp;rdquo;, dont je viens de parler. &lt;strong&gt;Mais&lt;/strong&gt; le manifeste Canal officiel embarque son propre flannel dans le même DaemonSet que calico-node. Si votre flannel est déjà installé et géré (par vous, par un opérateur, par un provider&amp;hellip;), vous ne voulez probablement pas le remplacer. Et l&amp;rsquo;opérateur Tigera (la méthode Helm &amp;ldquo;officielle&amp;rdquo;) ne supporte pas non plus le déploiement en mode policy-only sur un flannel existant. Bref, c&amp;rsquo;est faisable mais ça nécessite un peu d&amp;rsquo;effort. Flemme.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;kube-router&lt;/strong&gt;, kube-router peut fonctionner en mode firewall-only (&lt;code&gt;--run-firewall=true&lt;/code&gt;) et n&amp;rsquo;a besoin que d&amp;rsquo;iptables/ipset. C&amp;rsquo;est d&amp;rsquo;ailleurs ce que k3s utilise par défaut pour les NetworkPolicies. C&amp;rsquo;est la solution la plus légère (a priori ~50 Mo de RAM par node). Vérifiez que votre kernel dispose du module &lt;code&gt;ip_set&lt;/code&gt;, sinon ça ne fonctionnera pas.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cilium en mode CNI chaining&lt;/strong&gt;, C&amp;rsquo;est la solution que j&amp;rsquo;ai retenue et qu&amp;rsquo;on va détailler. Cilium s&amp;rsquo;attache aux interfaces veth créées par flannel et ajoute ses programmes eBPF pour le policy enforcement. Pas de dépendance à iptables ou ipset, et en bonus on récupère Hubble pour l&amp;rsquo;observabilité réseau.&lt;/p&gt;
&lt;h2 id="installer-cilium-en-mode-cni-chaining"&gt;Installer Cilium en mode CNI chaining
&lt;/h2&gt;&lt;h3 id="prérequis"&gt;Prérequis
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Un cluster Kubernetes fonctionnel avec flannel&lt;/li&gt;
&lt;li&gt;&lt;code&gt;helm&lt;/code&gt; v3+&lt;/li&gt;
&lt;li&gt;Un kernel &amp;gt;= 4.19 (idéalement &amp;gt;= 5.10 pour toutes les features eBPF)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="récupérer-la-configuration-cni-de-flannel"&gt;Récupérer la configuration CNI de flannel
&lt;/h3&gt;&lt;p&gt;Cilium en mode chaining doit connaître la configuration CNI existante. On va la récupérer depuis un node :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl debug node/&lt;span class="k"&gt;$(&lt;/span&gt;kubectl get nodes -o &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{.items[0].metadata.name}&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -it --image&lt;span class="o"&gt;=&lt;/span&gt;busybox -- cat /host/etc/cni/net.d/10-flannel.conflist
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Sur mon cluster, ça donne :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;cbr0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;cniVersion&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0.3.1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;plugins&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;flannel&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;delegate&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;hairpinMode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;isDefaultGateway&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;portmap&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;capabilities&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;portMappings&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Notez le champ &lt;code&gt;name&lt;/code&gt;&lt;/strong&gt; (ici &lt;code&gt;cbr0&lt;/code&gt;). On en aura besoin.&lt;/p&gt;
&lt;h3 id="créer-le-configmap-de-chaining"&gt;Créer le ConfigMap de chaining
&lt;/h3&gt;&lt;p&gt;On va créer un ConfigMap qui reprend la config flannel et y ajoute le plugin &lt;code&gt;cilium-cni&lt;/code&gt; en chaining :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ConfigMap&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;cni-configuration&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;kube-system&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cni-config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|-&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;name&amp;#34;: &amp;#34;cbr0&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;cniVersion&amp;#34;: &amp;#34;0.3.1&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;plugins&amp;#34;: [
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;type&amp;#34;: &amp;#34;flannel&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;delegate&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;hairpinMode&amp;#34;: true,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;isDefaultGateway&amp;#34;: true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;type&amp;#34;: &amp;#34;portmap&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;capabilities&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;portMappings&amp;#34;: true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;type&amp;#34;: &amp;#34;cilium-cni&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;chaining-mode&amp;#34;: &amp;#34;generic-veth&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; }&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Attention&lt;/strong&gt; : le champ &lt;code&gt;name&lt;/code&gt; doit correspondre à celui de votre &lt;em&gt;conflist&lt;/em&gt; flannel. Si le vôtre s&amp;rsquo;appelle &lt;code&gt;cni0&lt;/code&gt; ou autre chose, adaptez.&lt;/p&gt;
&lt;p&gt;Avant d&amp;rsquo;appliquer, vérifiez aussi que votre CNI utilise bien des interfaces &lt;strong&gt;veth&lt;/strong&gt; (c&amp;rsquo;est le cas par défaut avec flannel, mais mieux vaut s&amp;rsquo;en assurer). Depuis un node :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ip -d link &lt;span class="p"&gt;|&lt;/span&gt; grep veth
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Vous devriez voir des interfaces de type &lt;code&gt;veth&lt;/code&gt; correspondant à vos pods, par exemple :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;103: lxcb3901b7f9c02@if102: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; ...
veth addrgenmode eui64 numtxqueues 1 numrxqueues 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Si c&amp;rsquo;est bien le cas, le mode &lt;code&gt;generic-veth&lt;/code&gt; de Cilium fonctionnera. On applique :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl apply -f cilium-cni-configmap.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="installer-cilium-via-helm"&gt;Installer Cilium via Helm
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm repo add cilium https://helm.cilium.io/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm repo update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Voici les values pour le mode chaining :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# cilium-values.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;cni&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;chainingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;generic-veth&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;customConf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;configMap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;cni-configuration&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;install&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;routingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;native&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;enableIPv4Masquerade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;enableIPv6Masquerade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hubble&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;relay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Les points importants :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cni.chainingMode: generic-veth&lt;/code&gt;, c&amp;rsquo;est le mode chaining, Cilium s&amp;rsquo;attache aux interfaces veth existantes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cni.customConf: true&lt;/code&gt; + &lt;code&gt;cni.configMap&lt;/code&gt;, on fournit notre propre config CNI&lt;/li&gt;
&lt;li&gt;&lt;code&gt;routingMode: native&lt;/code&gt;, flannel gère le routage, pas Cilium&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enableIPv4Masquerade: false&lt;/code&gt;, flannel gère le masquerading&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hubble.enabled: true&lt;/code&gt;, l&amp;rsquo;observabilité réseau, c&amp;rsquo;est le gros bonus de Cilium&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm install cilium cilium/cilium --version 1.19.1 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --namespace kube-system &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -f cilium-values.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On attend que tout soit prêt :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl rollout status daemonset/cilium -n kube-system --timeout&lt;span class="o"&gt;=&lt;/span&gt;120s
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="vérification"&gt;Vérification
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; -n kube-system ds/cilium -- cilium status
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ce qui nous intéresse :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Kubernetes: Ok 1.35 (v1.35.0) [linux/amd64]
CNI Chaining: generic-veth
Cilium: Ok 1.19.1
Hubble: Ok
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;La ligne &lt;code&gt;CNI Chaining: generic-veth&lt;/code&gt; confirme que Cilium fonctionne en mode &lt;em&gt;chaining&lt;/em&gt; et ne remplace pas flannel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt; : les pods qui existaient avant l&amp;rsquo;installation de Cilium ne sont pas automatiquement gérés par Cilium. Il faut les redémarrer pour que Cilium attache ses programmes eBPF. Pensez à faire un &lt;code&gt;kubectl rollout restart&lt;/code&gt; de vos workloads de test (ou à les recréer).&lt;/p&gt;
&lt;h2 id="tester-les-networkpolicies"&gt;Tester les NetworkPolicies
&lt;/h2&gt;&lt;p&gt;C&amp;rsquo;est le moment de vérité. On re-applique notre deny-all :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl apply -f 01-deny-all-ingress.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; -n netpol-test-a client -- &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl -s --max-time &lt;span class="m"&gt;5&lt;/span&gt; http://server.netpol-test-b.svc.cluster.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Résultat : timeout&lt;/strong&gt; ! Cette fois, la NetworkPolicy est bien enforced. Le trafic est bloqué.&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai enchaîné avec les autres scénarios classiques de NetworkPolicy, et tout fonctionne :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Allow ingress sélectif par namespace&lt;/strong&gt;, en ajoutant une policy qui autorise le trafic depuis &lt;code&gt;netpol-test-a&lt;/code&gt; uniquement, le curl passe depuis ce namespace mais reste bloqué depuis &lt;code&gt;default&lt;/code&gt;. L&amp;rsquo;isolation par namespace fonctionne.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deny-all egress&lt;/strong&gt;, en bloquant tout le trafic sortant du client, même la résolution DNS est bloquée (timeout immédiat).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Allow egress sélectif&lt;/strong&gt;, en autorisant uniquement le DNS (port 53) et le serveur (port 80 dans le namespace &lt;code&gt;netpol-test-b&lt;/code&gt;), le curl vers le serveur passe mais &lt;code&gt;curl http://example.com&lt;/code&gt; reste bloqué.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bref, ingress, egress, sélectif par namespace, tout marche comme attendu.&lt;/p&gt;
&lt;h2 id="bonus--hubble-lobservabilité-réseau"&gt;Bonus : Hubble, l&amp;rsquo;observabilité réseau
&lt;/h2&gt;&lt;p&gt;C&amp;rsquo;est pour moi le vrai atout de Cilium par rapport aux alternatives. Hubble permet de voir en temps réel les flux réseau et les verdicts de policy :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; -n kube-system ds/cilium -- &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; hubble observe --namespace netpol-test-b --last &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;Mar 15 13:20:42.287: netpol-test-a/client:40066 (ID:9745) -&amp;gt;
netpol-test-b/server:80 (ID:22271)
policy-verdict:none ALLOWED (TCP Flags: SYN)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;On voit le pod source, le pod destination, le port, l&amp;rsquo;identité Cilium, et le verdict de policy. Quand vous debuggez une NetworkPolicy qui ne se comporte pas comme prévu, c&amp;rsquo;est vraiment pratique.&lt;/p&gt;
&lt;h2 id="combien-ça-coûte-en-ressources-"&gt;Combien ça coûte en ressources ?
&lt;/h2&gt;&lt;p&gt;Sur mon cluster (3 nodes), voici ce que Cilium consomme juste après installation :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Composant&lt;/th&gt;
&lt;th&gt;Par node&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cilium agent&lt;/td&gt;
&lt;td&gt;oui (DaemonSet)&lt;/td&gt;
&lt;td&gt;~160 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cilium-envoy&lt;/td&gt;
&lt;td&gt;oui (DaemonSet)&lt;/td&gt;
&lt;td&gt;~22 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cilium-operator&lt;/td&gt;
&lt;td&gt;non (2 replicas)&lt;/td&gt;
&lt;td&gt;~42 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hubble-relay&lt;/td&gt;
&lt;td&gt;non (1 replica)&lt;/td&gt;
&lt;td&gt;~16 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hubble-ui&lt;/td&gt;
&lt;td&gt;non (1 replica)&lt;/td&gt;
&lt;td&gt;~21 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Soit environ &lt;strong&gt;180 Mo par node&lt;/strong&gt; pour l&amp;rsquo;agent + envoy. J&amp;rsquo;ai pas de point de comparaison par rapport à Calico ou kube-router, mais ça me semble acceptable, et le fait de pouvoir avoir une vision complète sur la totalité des flux avec Hubble justifie largement le surcoût (à mon avis).&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Si jamais vous n&amp;rsquo;avez pas le choix et que vous devez composer avec flannel, et que vous voulez boucher le trou béant de sécurité que représente l&amp;rsquo;absence d&amp;rsquo;enforcement des Netpols, sachez donc qu&amp;rsquo;il est maintenant possible :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;d&amp;rsquo;activer l&amp;rsquo;ajout de l&amp;rsquo;implémentation officielle (même si je ne l&amp;rsquo;ai pas testée, ça doit marcher)&lt;/li&gt;
&lt;li&gt;de chaîner un autre CNI pour le faire&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;À défaut de l&amp;rsquo;avoir en CNI pour tout (tout bon cluster Kubernetes a Cilium comme CNI), Cilium en mode CNI chaining (generic-veth) est une solution plutôt sympa pour combler ce manque. Il ne touche pas au flannel existant, il s&amp;rsquo;y greffe. Et en bonus, vous récupérez Hubble pour l&amp;rsquo;observabilité réseau, ce qui est franchement appréciable.&lt;/p&gt;
&lt;p&gt;Have fun :)&lt;/p&gt;</description></item><item><title>Refonte de la page Conférences : data-driven avec Hugo</title><link>https://blog.zwindler.fr/2026/03/13/refonte-page-conferences-hugo/</link><pubDate>Fri, 13 Mar 2026 20:00:00 +0100</pubDate><guid>https://blog.zwindler.fr/2026/03/13/refonte-page-conferences-hugo/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/03/conferences-apres.webp" alt="Featured image of post Refonte de la page Conférences : data-driven avec Hugo" /&gt;&lt;p&gt;Grosse refonte de ma page &amp;ldquo;Conférences&amp;rdquo; qui était un bazar sans nom, dans lequel j&amp;rsquo;avais aussi mélangé les podcasts, les publications écrites, etc. Maintenant tout est présenté avec des &amp;ldquo;cartes&amp;rdquo;, et bien séparé en 3 pages distinctes.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tous les fichiers sources (YAML, layouts Hugo, CSS) sont disponibles dans &lt;a class="link" href="https://blog.zwindler.fr/misc/conferences-refonte/" &gt;/misc/conferences-refonte/&lt;/a&gt; si vous voulez reproduire ou vous en inspirer.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="le-problème"&gt;Le problème
&lt;/h2&gt;&lt;p&gt;Ma page &lt;a class="link" href="https://blog.zwindler.fr/conf%c3%a9rences/" &gt;Conférences&lt;/a&gt; était un &lt;strong&gt;gros fichier Markdown&lt;/strong&gt; monolithique. Talks, podcasts, publications, orga : tout mélangé dans des listes à puces, avec des doublons, et &lt;strong&gt;de plus en plus pénible à maintenir&lt;/strong&gt; (surtout depuis le passage en multi-langue FR/EN).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/03/conferences-avant.webp"
loading="lazy"
alt="La page Conférences avant la refonte - un long fichier Markdown avec des listes à puces"
&gt;&lt;/p&gt;
&lt;h2 id="lapproche--données--layouts--css"&gt;L&amp;rsquo;approche : données + layouts + CSS
&lt;/h2&gt;&lt;p&gt;Plutôt que de continuer à maintenir du Markdown, je suis passé en &lt;strong&gt;data-driven&lt;/strong&gt;, comme je l&amp;rsquo;ai fait pour mon side project &amp;ldquo;101 ways to deploy Kubernetes&amp;rdquo; :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Données&lt;/strong&gt; dans des fichiers YAML (&lt;code&gt;data/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Présentation&lt;/strong&gt; dans des layouts Hugo custom (&lt;code&gt;layouts/page/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Style&lt;/strong&gt; dans des CSS dédiés (&lt;code&gt;static/css/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pages Markdown&lt;/strong&gt; réduites au strict minimum (front matter uniquement)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Et au passage, j&amp;rsquo;ai découpé &lt;strong&gt;une page fourre-tout en trois&lt;/strong&gt; : Conférences, Podcasts &amp;amp; Lives, Publications.&lt;/p&gt;
&lt;h2 id="les-données-yaml"&gt;Les données YAML
&lt;/h2&gt;&lt;h3 id="dataconferencesyaml"&gt;&lt;code&gt;data/conferences.yaml&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;Le plus gros fichier. L&amp;rsquo;idée clé : un &lt;strong&gt;talk&lt;/strong&gt; (sujet unique) peut être présenté à &lt;strong&gt;plusieurs events&lt;/strong&gt;. Fini la duplication.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;talks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;k8s-scheduling&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Limits, Requests, QoS, PriorityClasses, ...&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Limits, Requests, QoS, PriorityClasses: ...&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cospeaker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Quentin Joly&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/talks/2025-lrqppobcqvpsslsdk/...&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;talk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;k8s-scheduling &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# référence l&amp;#39;id du talk&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;DevoxxFR 2026&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ld"&gt;2026-03-22&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;conference&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;talk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;k8s-scheduling&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;TNT 26&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ld"&gt;2026-02-12&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;conference&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;video&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;https://www.youtube.com/watch?v=...&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;organizer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Membre de l&amp;#39;équipe d&amp;#39;organisation du Meetup CNCF Bordeaux&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Les titres sont bilingues (FR/EN), Hugo sélectionne la bonne langue automatiquement. Les fichiers &lt;code&gt;data/podcasts.yaml&lt;/code&gt; et &lt;code&gt;data/publications.yaml&lt;/code&gt; suivent le même principe (voir les &lt;a class="link" href="https://blog.zwindler.fr/misc/conferences-refonte/" &gt;fichiers sources&lt;/a&gt;).&lt;/p&gt;
&lt;h2 id="les-pages-markdown-minimalistes"&gt;Les pages Markdown minimalistes
&lt;/h2&gt;&lt;p&gt;L&amp;rsquo;ancienne page &lt;code&gt;content/page/conferences.md&lt;/code&gt; faisait 100+ lignes avec le contenu complet. Voici la nouvelle :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Conférences&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;authors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;zwindler&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;page&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;conferences&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ld"&gt;2022-03-14T18:00:00&lt;/span&gt;&lt;span class="m"&gt;+00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="m"&gt;40&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;messages&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;toc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Le point important, c&amp;rsquo;est &lt;code&gt;layout: conferences&lt;/code&gt; dit à Hugo d&amp;rsquo;utiliser &lt;code&gt;layouts/page/conferences.html&lt;/code&gt; au lieu du layout par défaut. Même chose pour &lt;code&gt;podcasts&lt;/code&gt; et &lt;code&gt;publications&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="le-layout-hugo"&gt;Le layout Hugo
&lt;/h2&gt;&lt;p&gt;Le layout charge les données YAML, calcule des stats, et génère du HTML. Si vous avez déjà manipulé du GoTemplate (helm notamment), ça vous semblera facile à comprendre.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chargement et indexation des talks :&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.Site.Data.conferences&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$talks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dict&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$data&lt;/span&gt;&lt;span class="na"&gt;.talks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$talks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;merge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$talks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dict&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Compteur dynamique&lt;/strong&gt; ex. combien de fois un talk a été présenté :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$talkId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$data&lt;/span&gt;&lt;span class="na"&gt;.events&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.talk&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$talkId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;talk-card-count&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Présenté &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;x&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Événements groupés par année&lt;/strong&gt;, les 2 plus récentes dépliées, le reste en archive pliable via &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$i&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$year&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$sortedYears&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;lt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;details&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;year-group&amp;#34;&lt;/span&gt; &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;details&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;year-group&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Les événements &lt;strong&gt;à venir&lt;/strong&gt; (date dans le futur) reçoivent automatiquement un badge &amp;ldquo;à venir&amp;rdquo;. Les layouts complets sont dans les &lt;a class="link" href="https://blog.zwindler.fr/misc/conferences-refonte/" &gt;fichiers sources&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="le-css--des-cartes-partout"&gt;Le CSS : des cartes partout
&lt;/h2&gt;&lt;p&gt;Le design repose sur des &lt;strong&gt;cartes&lt;/strong&gt; CSS grid, avec des badges colorés par type (conf, meetup, BBL), du responsive, et un support dark mode. L&amp;rsquo;essentiel :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;talks-grid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="kc"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;minmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;280&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="kt"&gt;rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;talk-card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="kt"&gt;rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;border-color&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="kt"&gt;s&lt;/span&gt; &lt;span class="kc"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;box-shadow&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="kt"&gt;s&lt;/span&gt; &lt;span class="kc"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;type-conference&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#dbeafe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#1e40af&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;type-meetup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#dcfce7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#166534&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;type-bbl&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#fef9c3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#854d0e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;type-upcoming&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#fee2e2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#991b1b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Les CSS complets gèrent aussi le dark mode et les breakpoints mobile. Ils sont dans les &lt;a class="link" href="https://blog.zwindler.fr/misc/conferences-refonte/" &gt;fichiers sources&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="bilan"&gt;Bilan
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Avant&lt;/th&gt;
&lt;th&gt;Après&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pages&lt;/td&gt;
&lt;td&gt;1 fourre-tout&lt;/td&gt;
&lt;td&gt;3 spécialisées&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Données&lt;/td&gt;
&lt;td&gt;Markdown libre&lt;/td&gt;
&lt;td&gt;YAML structuré&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplication&lt;/td&gt;
&lt;td&gt;Oui&lt;/td&gt;
&lt;td&gt;Non (relation talk → events)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stats&lt;/td&gt;
&lt;td&gt;Aucune&lt;/td&gt;
&lt;td&gt;Automatiques&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design&lt;/td&gt;
&lt;td&gt;Listes à puces&lt;/td&gt;
&lt;td&gt;Cartes + badges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-langue&lt;/td&gt;
&lt;td&gt;Copier-coller&lt;/td&gt;
&lt;td&gt;Titres FR/EN dans le YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Archives&lt;/td&gt;
&lt;td&gt;Tout d&amp;rsquo;un bloc&lt;/td&gt;
&lt;td&gt;Années pliables&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;C&amp;rsquo;était un gros morceau : 19 fichiers et ~2400 lignes (une grosse partie du taf, en particulier la traduction markdown =&amp;gt; YAML et le CSS, a été faite par un LLM), mais la logique est simple : &lt;strong&gt;données dans YAML, présentation dans les layouts, style dans le CSS&lt;/strong&gt;. Hugo fait le lien au build, sans JavaScript.&lt;/p&gt;
&lt;p&gt;Pour ajouter un nouveau talk maintenant, il me suffit d&amp;rsquo;ajouter une entrée dans &lt;code&gt;data/conferences.yaml&lt;/code&gt; et les stats se mettent à jour toutes seules.&lt;/p&gt;
&lt;p&gt;Avouez que c&amp;rsquo;est quand même plus propre qu&amp;rsquo;un copier-coller dans plusieurs fichiers Markdown :).&lt;/p&gt;</description></item><item><title>Podcasts &amp; Lives</title><link>https://blog.zwindler.fr/podcasts-lives/</link><pubDate>Fri, 13 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog.zwindler.fr/podcasts-lives/</guid><description/></item><item><title>Publications</title><link>https://blog.zwindler.fr/publications/</link><pubDate>Fri, 13 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog.zwindler.fr/publications/</guid><description/></item><item><title>KubeSolo sur Raspberry Pi Zero : mon article dans Sysops Pratique</title><link>https://blog.zwindler.fr/2026/03/09/kubesolo-raspberry-pi-zero-sysops-pratique/</link><pubDate>Mon, 09 Mar 2026 18:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2026/03/09/kubesolo-raspberry-pi-zero-sysops-pratique/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/03/sysops-pratique.webp" alt="Featured image of post KubeSolo sur Raspberry Pi Zero : mon article dans Sysops Pratique" /&gt;&lt;h2 id="kubernetes-sur-512-mo-de-ram-cest-possible"&gt;Kubernetes sur 512 Mo de RAM, c&amp;rsquo;est possible
&lt;/h2&gt;&lt;p&gt;Après avoir teasé sur Bluesky, je peux enfin vous annoncer (fièrement) la publication de mon article dans le numéro de mars/avril de &lt;strong&gt;Sysops Pratique&lt;/strong&gt; (le successeur de Linux Pratique, magazine bien connu des administrateurs système francophones).&lt;/p&gt;
&lt;p&gt;Le sujet : faire tourner &lt;strong&gt;&lt;a class="link" href="https://github.com/portainer/kubesolo" target="_blank" rel="noopener"
&gt;KubeSolo&lt;/a&gt;&lt;/strong&gt;, une distribution extra-légère de Kubernetes développée par Portainer, sur un &lt;strong&gt;Raspberry Pi Zero 2 W&lt;/strong&gt; et ses maigres 512 Mo de RAM.&lt;/p&gt;
&lt;h2 id="kubesolo-cest-quoi-"&gt;KubeSolo, c&amp;rsquo;est quoi ?
&lt;/h2&gt;&lt;p&gt;KubeSolo est une distribution Kubernetes minimaliste, pensée pour les environnements très contraints (IoT, edge). Contrairement à K3s ou MicroK8s qui sont économes, mais tout de même pas assez pour un Pi Zero, KubeSolo vise le strict minimum pour faire tourner un kubelet et un runtime de conteneurs.&lt;/p&gt;
&lt;p&gt;À vrai dire, même KubeSolo seul ne suffisait pas pour mon Raspberry Pi Zero. Il a fallu pas mal d&amp;rsquo;optimisations supplémentaires pour que tout rentre dans 512 Mo. Réduction de la mémoire allouée au GPU, configuration de zswap, ajustements du kernel&amp;hellip; bref, de la bonne bidouille d&amp;rsquo;admin sys comme on les aime.&lt;/p&gt;
&lt;p&gt;Si vous avez lu mon article de décembre sur l&amp;rsquo;&lt;a class="link" href="https://blog.zwindler.fr/2025/12/23/alpine-linux-raspberry-pi-headless/" &gt;installation d&amp;rsquo;Alpine Linux en mode headless sur Raspberry Pi&lt;/a&gt;, c&amp;rsquo;était en quelque sorte le préambule de cette expérimentation. J&amp;rsquo;y avais déjà exploré les optimisations mémoire possibles sur le Pi Zero.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/12/rpizero.webp"
loading="lazy"
alt="Le Raspberry Pi Zero 2 W, dans toute sa splendeur"
&gt;&lt;/p&gt;
&lt;h2 id="doom-dans-kubernetes-parce-que-pourquoi-pas-"&gt;Doom dans Kubernetes, parce que pourquoi pas ?
&lt;/h2&gt;&lt;p&gt;Et parce que faire tourner Kubernetes sur un nano-ordinateur c&amp;rsquo;est bien, mais que ce n&amp;rsquo;est pas suffisamment absurde à mon goût, j&amp;rsquo;ai aussi réussi à y faire tourner &lt;strong&gt;kubedoom&lt;/strong&gt; : une version de Doom packagée pour Kubernetes avec un serveur VNC intégré.&lt;/p&gt;
&lt;p&gt;Oui, c&amp;rsquo;est complètement inutile. Mais avouez que c&amp;rsquo;est beau.&lt;/p&gt;
&lt;h2 id="où-trouver-le-magazine-"&gt;Où trouver le magazine ?
&lt;/h2&gt;&lt;p&gt;Sysops Pratique est disponible en kiosque et &lt;a class="link" href="https://connect.ed-diamond.com/linux-pratique/lp-154/un-cluster-kubernetes-sur-raspberry-pi-zero-2w-vraiment" target="_blank" rel="noopener"
&gt;en version numérique&lt;/a&gt;. Si le sujet vous intéresse (Kubernetes, Raspberry Pi, optimisation système, ou juste l&amp;rsquo;envie de voir jusqu&amp;rsquo;où on peut pousser le bouchon), je vous invite à aller y jeter un œil.&lt;/p&gt;
&lt;p&gt;C&amp;rsquo;est aussi une bonne manière de soutenir la presse technique francophone, qui n&amp;rsquo;a pas la vie facile.&lt;/p&gt;
&lt;h2 id="à-propos-de-la-rémunération"&gt;À propos de la rémunération
&lt;/h2&gt;&lt;p&gt;Je tiens à être transparent sur ce sujet. Écrire dans un magazine, c&amp;rsquo;est rémunéré. J&amp;rsquo;ai beaucoup de chance, je n&amp;rsquo;ai pas besoin de cet argent.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/03/revenu.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai donc reversé la totalité des revenus nets de cet article (445€, donc) au &lt;a class="link" href="https://www.planning-familial.org/fr/le-planning-familial-de-gironde-33" target="_blank" rel="noopener"
&gt;Planning Familial de la Gironde&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Pourquoi ? Parce que nous vivons dans un monde de violences sexistes et sexuelles, et que nous assistons au recul des droits des femmes et des communautés LGBT (notamment des personnes transgenres et non binaires), attaqués par les mouvements d&amp;rsquo;extrême droite, masculinistes, identitaires et autres réactionnaires de tout poil.&lt;/p&gt;
&lt;p&gt;Le Planning Familial fait un travail essentiel face à ces menaces (droit à l&amp;rsquo;information, éducation sexuelle, défense du droit à l&amp;rsquo;IVG, combat des stéréotypes de genre), et c&amp;rsquo;est une des meilleures façons que j&amp;rsquo;ai trouvées de contribuer à mon échelle.&lt;/p&gt;</description></item></channel></rss>