<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Posts on Rémi Tech Notes</title><link>https://www.vrchr.fr/posts/</link><description>Recent content in Posts on Rémi Tech Notes</description><generator>Hugo</generator><language>fr-fr</language><lastBuildDate>Thu, 06 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.vrchr.fr/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Docker sbx et Claude Code : mémoire et config custom depuis l'hôte</title><link>https://www.vrchr.fr/posts/2026/08/06/sbx-kit-project-host/</link><pubDate>Thu, 06 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2026/08/06/sbx-kit-project-host/</guid><description>Utilisation d&amp;#39;un kit &amp;#39;mixin&amp;#39; sbx qui persiste la mémoire et les configs custom Claude Code sur l&amp;#39;hôte, pour que rien ne meure quand la sandbox disparaît.</description><content:encoded><![CDATA[<p>Depuis quelques temps, j'utilise comme beaucoup d'entre vous Claude Code pour accélérer
la gestion de mes tâches techniques. Je l'utilise surtout pour m'aider à résoudre, comprendre,
améliorer mes connaissances sur les sujets que je traite.</p>
<p>Utilisateur de la CLI, le fait de donner tout pouvoir à un outil tel que Claude ne me plaisait pas trop,
et j'ai donc naturellement commencé à utiliser Docker Sandbox (<code>sbx</code>) qui me permet, entre autres, d'avoir :</p>
<ul>
<li>Une isolation du filesystem (sbx n'a accès qu'au dossier courant, et il ne &quot;pourrit&quot; pas mon système hôte)</li>
<li>Pas d'accès à toutes mes variables d'environnement</li>
<li>Un contrôle sur les flux réseau sortants</li>
</ul>
<p>Mais le problème, c'est qu'une sandbox <code>sbx</code>, c'est une micro-vm qui peut être éphémère.
Une fois la session Claude terminée, vous pouvez la supprimer avec <code>sbx rm</code>, son historique et sa mémoire avec.</p>
<blockquote>
<p>⚠️ Ce que j'ai mis en place à date (été 2026) peut avoir évolué depuis, les projets sont très actifs !</p>
</blockquote>
<h2 id="les-problèmes">Les problèmes</h2>
<h3 id="la-mémoire">La mémoire</h3>
<p>Claude Code stocke la mémoire et l'historique de chaque session dans
<code>~/.claude/projects/&lt;slug&gt;/</code>. Oui, vous pouvez vérifier chez vous, vous aurez un dossier
dont le nom dérive du chemin du workspace pour chaque dossier où vous avez lancé une session Claude.
Dans une sandbox, ce dossier vit sur un volume Docker <strong>lié au cycle de vie de la sandbox</strong>.
Lorsqu'on supprime la sandbox (<code>sbx rm</code>), on perd tout : la mémoire accumulée au fil des sessions,
les transcripts <code>.jsonl</code>, la config perso.</p>
<p>Et donc vous voyez venir la tuile : au prochain <code>sbx run</code> alors que la sandbox a été détruite, tout est oublié 🫤.</p>
<h3 id="la-config-custom">La config custom</h3>
<p>Autre problème, j'utilise Claude à <strong>ma façon</strong>, et j'aimerais bien avoir <em>ma façon</em>
aussi dans la sandbox, comme par exemple :</p>
<ul>
<li>Mon propre <code>CLAUDE.md</code></li>
<li>Ma config propre (thème clair, en français, pas de télémétrie)</li>
<li>Mes hooks activés (<a href="https://github.com/rtk-ai/rtk">RTK</a>)</li>
</ul>
<p>Avec <code>sbx run claude</code>, Claude Code est lancé en mode <em>YOLO</em>, mais pas plus.
Il va falloir trouver un moyen de copier depuis l'hôte.</p>
<blockquote>
<p><em>TL;DR :</em> Vous pouvez retrouver mon kit <a href="https://github.com/rverchere/sbx-kit-claude-project-host">ici</a>
si vous avez la flemme de lire la suite 🫠.</p>
</blockquote>
<h2 id="une-solution--les-kits-mixin">Une solution : les kits mixin</h2>
<p>Docker sbx permet d'étendre les fonctionnalités de base avec des
<a href="https://docs.docker.com/ai/sandboxes/customize/kits/">kits</a>, notamment les <em>kits mixin</em>.
C'est donc cela que je vais utiliser pour résoudre mes 2 problèmes !</p>
<blockquote>
<p>J'utilise la spec <code>v2</code> des kits, livrée avec <code>sbx</code> 0.38.0, sortie le jour même de cet article.</p>
</blockquote>
<h3 id="sauver-la-mémoire-sur-lhôte">Sauver la mémoire sur l'hôte</h3>
<p>Un kit <code>sbx</code> ne peut pas monter un dossier arbitraire de l'hôte.
Les <strong>seuls</strong> chemins réellement persistants côté hôte dans une sandbox sont le workspace lui-même
et les <a href="https://docs.docker.com/ai/sandboxes/workflows/#share-agent-skills">skills</a> (au moins en version <code>v0.38</code>).
Tout le reste (<code>~/.claude/projects</code>, <code>sessions</code>, <code>todos</code>) est un volume déclaré par l'agent de base, donc lié
à la sandbox.</p>
<p>Ce qu'on va alors faire, c'est simplement de remplacer <code>~/.claude/projects/&lt;slug&gt;</code> par un <strong>lien symbolique</strong>
pointant vers <code>.claude-project/</code> à la racine du workspace : lorsque ma sandbox sera créée,
un dossier <code>.claude-project/</code> sera créé à la racine du workspace, et le lien symbolique sera mis en place.</p>
<p>Côté kit, cela se fait via un script exécuté à chaque démarrage de sandbox dans le fichier <a href="https://github.com/rverchere/sbx-kit-claude-project-host/blob/0.0.1/spec.yaml#L80"><code>spec.yaml</code></a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">startup</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span>- <span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Links ~/.claude/projects/&lt;slug&gt; to the workspace, excludes it from git</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">user</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">      </span>- <span class="l">sh</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">      </span>- -<span class="l">c</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">      </span>- <span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="sd">        # Mon super code généré par Claude pour faire un symlink</span><span class="w">
</span></span></span></code></pre></div><p>Ce script tourne à chaque démarrage de sandbox, mémoire et transcripts atterrissent alors
physiquement sur mon Mac, visibles dans le Finder, survivent à <code>sbx rm</code>.</p>
<p>J'ai défini un dossier <code>.claude-project/</code>, qui est exclu localement via <code>.git/info/exclude</code>,
cela est aussi géré au niveau du <a href="https://github.com/rverchere/sbx-kit-claude-project-host/blob/0.0.1/spec.yaml#L144">kit</a> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Ignore local to the clone, without touching the versioned .gitignore.</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># --git-common-dir (not --git-dir): in a linked worktree, --git-dir</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># points to .git/worktrees/&lt;name&gt;, where info/exclude is not read by git.</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nv">GITDIR</span><span class="o">=</span><span class="k">$(</span>git -C <span class="s2">&#34;</span><span class="nv">$WS</span><span class="s2">&#34;</span> rev-parse --git-common-dir 2&gt;/dev/null<span class="k">)</span> <span class="o">||</span> <span class="nb">exit</span> <span class="m">0</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$GITDIR</span><span class="s2">&#34;</span> in /*<span class="o">)</span> <span class="p">;;</span> *<span class="o">)</span> <span class="nv">GITDIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$WS</span><span class="s2">/</span><span class="nv">$GITDIR</span><span class="s2">&#34;</span> <span class="p">;;</span> <span class="k">esac</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="nv">$GITDIR</span><span class="s2">/info&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">grep -qxF <span class="s1">&#39;/.claude-project/&#39;</span> <span class="s2">&#34;</span><span class="nv">$GITDIR</span><span class="s2">/info/exclude&#34;</span> 2&gt;/dev/null <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="o">||</span> <span class="nb">echo</span> <span class="s1">&#39;/.claude-project/&#39;</span> &gt;&gt; <span class="s2">&#34;</span><span class="nv">$GITDIR</span><span class="s2">/info/exclude&#34;</span>
</span></span></code></pre></div><p>Autre point important, avec <strong><code>agentInstructions</code></strong> je vais passer des instructions spécifiques
pour que Claude Code gère bien ce dossier <code>.claude-project/</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">agentInstructions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">content</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="sd">    ## Host-persisted project directory</span><span class="w">
</span></span></span></code></pre></div><p>Avec cela, lorsque la sandbox est arrêtée, supprimée, relancée, le dossier <code>.claude-project/</code> persiste avec ce qu'il faut !
Fini la perte de mémoire en environnement cloisonné 🧠 !</p>
<h3 id="récupérer-ma-config-perso">Récupérer ma config perso</h3>
<p>Le symlink règle la mémoire, mais chaque <code>sbx run</code> repart aussi avec un <code>settings.json</code> vierge.
Même mécanique ici, un script qui merge un fichier JSON custom avec celui par défaut à chaque démarrage grâce à <code>jq</code>.</p>
<p>Voici mon fichier <code>settings.custom.json</code> (extrait) qui sera fusionné :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;theme&#34;</span><span class="p">:</span> <span class="s2">&#34;light-ansi&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&#34;statusLine&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;command&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;~/.claude/statusline.sh&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="nt">&#34;env&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nt">&#34;CLAUDE_CODE_ENABLE_TELEMETRY&#34;</span><span class="p">:</span> <span class="s2">&#34;0&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nt">&#34;language&#34;</span><span class="p">:</span> <span class="s2">&#34;French&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Et toujours pareil, le merge sera effectué au démarrage de la sandbox, défini <a href="https://github.com/rverchere/sbx-kit-claude-project-host/blob/0.0.1/spec.yaml#L132">ici</a> dans le <code>spec.yaml</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="ln">1</span><span class="cl">jq --arg dir <span class="s2">&#34;</span><span class="nv">$TARGET</span><span class="s2">&#34;</span> --slurpfile kit <span class="s2">&#34;</span><span class="nv">$CUSTOMSETTINGS</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl">   <span class="s1">&#39;. * {autoMemoryDirectory: $dir} * $kit[0]&#39;</span> <span class="s2">&#34;</span><span class="nv">$SETTINGS</span><span class="s2">&#34;</span> &gt; <span class="s2">&#34;</span><span class="nv">$TMP</span><span class="s2">&#34;</span>
</span></span></code></pre></div><h3 id="ajouter-mes-hooks-rtk-surtout">Ajouter mes hooks (RTK surtout)</h3>
<p>On continue, cette fois-ci avec les hooks. Vous avez compris le principe maintenant,
du bon script shell dans le <code>spec.yaml</code> qui installe le binaire et enregistre son hook,
mais au <code>setup</code> cette fois, pour éviter d'installer <code>RTK</code> à chaque (re)démarrage de la sandbox !</p>
<blockquote>
<p>J'aurais pu utiliser ici les <a href="https://docs.docker.com/ai/sandboxes/customize/templates/">templates</a> pour inclure
tous les tools / binaires nécessaires par défaut dans l'image de la sandbox, mais là ça m'évite de devoir gérer une image personnalisée.</p>
</blockquote>
<p>Alors pour le moment c'est un peu sale, car on a &quot;pin&quot; la version de <code>RTK</code> à installer, pour éviter un <code>curl | sh</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">setup</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">install</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span>- <span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Installs rtk (pinned binary, checksum verified) and its Bash hook</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">      </span><span class="nt">user</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1000&#34;</span><span class="w"> </span><span class="c"># Le user n&#39;est pas root, mais 1000 par défaut</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">      </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="sd">        # Ma super installation de RTK</span><span class="w">
</span></span></span></code></pre></div><p>Le hook RTK a besoin de définir des ordres dans le fichier <code>CLAUDE.md</code>, on voit ça juste après.</p>
<h3 id="mon-claudemd">Mon <code>CLAUDE.md</code></h3>
<p>Au fur et à mesure de mon utilisation de Claude, j'ai créé mon propre <code>CLAUDE.md</code> pour que l'outil me réponde
selon <strong>mes</strong> préférences, et j'aimerais bien le retrouver dans mes sandboxes.</p>
<p>Cette fois-ci, pas la peine de scripter, le kit permet de copier simplement des fichiers depuis
le répertoire <code>files/home/.claude</code> vers le répertoire <code>~/.claude/</code> de la sandbox.</p>
<p>&quot;Sauf que&quot;, mon fichier à moi <code>CLAUDE.md</code> est personnel (profil, stack de référence, décisions actées,
conventions de travail), pas forcément lié au projet ou à la sandbox.
J'ai donc décidé :</p>
<ul>
<li>de le nommer <code>PERSONA.md</code>, et de faire une inclusion via <code>@-import</code>.</li>
<li>d'avoir ce <code>PERSONA.md</code> <em>placeholder</em> dans le kit mis à disposition.</li>
<li>d'avoir <em>mon</em> <code>PERSONA.md</code> synchronisé avec <code>mise run claude</code> via une <code>task</code> personnalisée (voir plus bas).</li>
</ul>
<p>J'ai ensuite un <code>@RTK.md</code> : <a href="https://github.com/rtk-ai/rtk">rtk</a> en a besoin. Ce qui nous donne au final un fichier assez sommaire :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="ln">1</span><span class="cl"><span class="cm">&lt;!-- PERSONA, mes infos &#34;persos&#34; --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="ni">@PERSONA</span>.md
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="cm">&lt;!-- Hook RTK (https://github.com/rtk-ai/rtk) --&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="ni">@RTK</span>.md
</span></span></code></pre></div><h3 id="synchro-de-mon-personamd">Synchro de mon <code>PERSONA.md</code></h3>
<p>Pour utiliser mon vrai <code>~/.claude/PERSONA.md</code>, je le synchronise avec <code>mise run claude</code>,
dont voici la <code>task</code> correspondante. Au moins si besoin d'être utilisée ailleurs, elle n'est pas exclusive au kit :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">#MISE description=&#34;Sandbox claude-project-host : new if absent, resume + attach if present&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">#MISE dir=&#34;{{cwd}}&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">#MISE tools={ &#34;github:docker/sbx-releases&#34; = &#34;prefix:0.38&#34; }</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">#USAGE arg &#34;&lt;workspace&gt;&#34; help=&#34;Chemin du workspace a monter&#34; default=&#34;.&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># Copy PERSONA.md to the kit directory</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nv">kit_dir</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/sbx/claude-project-host&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">cp <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.claude/PERSONA.md&#34;</span> <span class="s2">&#34;</span><span class="nv">$kit_dir</span><span class="s2">/files/home/.claude/PERSONA.md&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nv">ws</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span><span class="nb">cd</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">usage_workspace</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="nb">pwd</span><span class="k">)</span><span class="s2">&#34;</span> <span class="o">||</span> <span class="o">{</span> <span class="nb">echo</span> <span class="s2">&#34;Workspace not found: &#39;</span><span class="si">${</span><span class="nv">usage_workspace</span><span class="si">}</span><span class="s2">&#39;&#34;</span> &gt;<span class="p">&amp;</span>2<span class="p">;</span> <span class="nb">exit</span> 1<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nv">name</span><span class="o">=</span><span class="s2">&#34;claude-</span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$ws</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># Check if the sandbox is already present, reattach if so</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">if</span> sbx ls <span class="p">|</span> awk <span class="s1">&#39;{print $1}&#39;</span> <span class="p">|</span> grep -Fqx <span class="s2">&#34;</span><span class="nv">$name</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Sandbox &#39;</span><span class="nv">$name</span><span class="s2">&#39; present, reattaching...&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  sbx run <span class="s2">&#34;</span><span class="nv">$name</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"># Otherwise, create a new sandbox with the toolkit</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Sandbox &#39;</span><span class="nv">$name</span><span class="s2">&#39; absent, creating...&#34;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  sbx run --name <span class="s2">&#34;</span><span class="nv">$name</span><span class="s2">&#34;</span> --kit <span class="s2">&#34;</span><span class="nv">$kit_dir</span><span class="s2">&#34;</span> claude <span class="s2">&#34;</span><span class="nv">$ws</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><h2 id="en-plus">En plus</h2>
<h3 id="quelques-bonus">Quelques bonus</h3>
<p>Tant qu'à faire, je rajoute des instructions spécifiques dans mon kit :</p>
<ul>
<li>Faire des &quot;Session summaries&quot; : écrire un résumé après chaque choix / débat important,
au format Markdown Obsidian pour que je puisse l'intégrer facilement dans mon coffre de notes.</li>
<li>Pour le moment pas beaucoup plus 😅</li>
</ul>
<h3 id="distribution-du-kit">Distribution du kit</h3>
<p>Le mixin a été pensé pour mon usage perso, en français. Pour le publier, j'ai séparé le
mécanisme (générique, à partager) du contenu (personnel, à ne pas partager).</p>
<p>Deux fichiers concentraient le problème :</p>
<ul>
<li><strong><code>PERSONA.md</code></strong> — on l'a vu plus haut, le kit publié embarque désormais un <strong>placeholder</strong> (les
sections à remplir, sans contenu), et mon vrai <code>PERSONA.md</code> reste local, resynchronisé par une
tâche <code>mise run claude</code> qui vit hors du repo.</li>
<li><strong><code>&quot;language&quot;: &quot;French&quot;</code></strong> dans les préférences répliquées — un défaut personnel que je garde
pour mon propre usage, à charge pour quiconque fork le kit de le changer pour le sien.</li>
</ul>
<p>Et pour celles et ceux qui ont lu jusque-là, je remets le lien du dépôt : <a href="https://github.com/rverchere/sbx-kit-claude-project-host">https://github.com/rverchere/sbx-kit-claude-project-host</a></p>
<p>Amusez-vous bien avec Docker Sandbox et Claude Code !</p>
<h2 id="liens">Liens</h2>
<ul>
<li>Spec des kits <code>sbx</code> : <a href="https://github.com/docker/sbx-kits-contrib/blob/main/spec/SPEC-v2.md">SPEC-v2.md</a></li>
<li>Kits communautaires : <a href="https://github.com/docker/sbx-kits-contrib">sbx-kits-contrib</a></li>
<li>Hook RTK : <a href="https://github.com/rtk-ai/rtk">rtk-ai/rtk</a></li>
</ul>]]></content:encoded></item><item><title>Envoy Gateway : 1 seul LoadBalancer avec MergeGateways</title><link>https://www.vrchr.fr/posts/2026/07/29/envoy-gateway-merge-gateways/</link><pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2026/07/29/envoy-gateway-merge-gateways/</guid><description>Éviter la prolifération de LoadBalancers avec Envoy Gateway : la feature mergeGateways pour partager un seul Service LB entre plusieurs Gateways d&amp;#39;un cluster Kubernetes.</description><content:encoded><![CDATA[<p>Dernier article sur Envoy Gateway ! Aujourd'hui, comment éviter d'avoir un Service LoadBalancer par Gateway déployée. La réponse d'Envoy Gateway : <strong><code>mergeGateways</code></strong>.</p>
<p>Lorsque j'ai migré depuis Ingress NGINX, j'ai été un peu dérouté avec la notion de <code>HTTPRoutes</code>, <code>Gateways</code>, etc. Et j'ai remarqué que pour chaque <code>Gateway</code> définie dans le cluster, Envoy Gateway génère son propre déploiement Envoy et son propre <code>Service</code> de type <code>LoadBalancer</code>. Si sur le principe c'est cohérent avec la spec, j'aimerais un moyen de mutualiser les LoadBalancers entre les <code>Gateways</code> du cluster, pour notamment économiser des coûts de cloud (et les IPs publiques).</p>
<h2 id="la-problématique--un-loadbalancer-par-gateway">La problématique : un LoadBalancer par Gateway</h2>
<p>En théorie, les équipes devs n'ont pas à gérer les <code>Gateways</code>, seulement les <code>Routes</code>, c'est aux ops de gérer cette partie. Mais parfois on peut donner de l'autonomie aux équipes, et qu'elles gèrent leurs propres <code>Gateways</code> avec leur policies.
Sans <code>mergeGateways</code>, le schéma est le suivant : chaque équipe qui déploie une <code>Gateway</code> pour son application se retrouve avec son propre LB provisionné côté cloud, sa propre IP.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">Gateway &#34;eg&#34; (envoy-gateway-system)      → Service LB → IP 203.0.113.10
</span></span><span class="line"><span class="ln">2</span><span class="cl">Gateway &#34;app1-gw&#34;                        → Service LB → IP 203.0.113.11
</span></span><span class="line"><span class="ln">3</span><span class="cl">Gateway &#34;app2-gw&#34;                        → Service LB → IP 203.0.113.12
</span></span><span class="line"><span class="ln">4</span><span class="cl">Gateway &#34;monitoring-gw&#34; (monitoring)     → Service LB → IP 203.0.113.13
</span></span><span class="line"><span class="ln">5</span><span class="cl">[...]
</span></span></code></pre></div><p>Ce comportement est différent d'avant avec les ingress, où j'avais 1 seul LB pour toutes mes ressources. On va voir comment retrouver cette façon de faire.</p>
<h2 id="la-solution--mergegateways">La solution : <code>mergeGateways</code></h2>
<p>Envoy Gateway propose via la ressource <code>EnvoyProxy</code> un paramètre <code>mergeGateways</code>. Quand il est à <code>true</code>, toutes les <code>Gateway</code> utilisant la même <code>GatewayClass</code> partagent <strong>un seul déploiement Envoy</strong> et <strong>un seul <code>Service</code> LoadBalancer</strong>.</p>
<p><img alt="Fusion" class="zoomable" decoding="async" loading="lazy" src="/2026/07/2026-07-29-envoy-gateway-fusion.jpg"></p>
<h3 id="la-ressource-envoyproxy">La ressource <code>EnvoyProxy</code></h3>
<p>C'est ici qu'on active la fusion des Gateways, et qu'on configure le <code>Service</code> LB partagé :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># envoyproxy-custom-proxy-config.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.envoyproxy.io/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">EnvoyProxy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">custom-proxy-config</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">envoy-gateway-system</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">mergeGateways</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="c"># &lt;-- là</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">provider</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Kubernetes</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">kubernetes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">envoyService</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">LoadBalancer</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span><span class="nt">envoyDeployment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span></code></pre></div><h3 id="le-gatewayclass-qui-référence-lenvoyproxy">Le <code>GatewayClass</code> qui référence l'<code>EnvoyProxy</code></h3>
<p>La <code>GatewayClass</code> fait le lien entre les <code>Gateway</code> et notre configuration <code>EnvoyProxy</code>, via <code>parametersRef</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># gatewayclass-eg.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">GatewayClass</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">controllerName</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.envoyproxy.io/gatewayclass-controller</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">parametersRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.envoyproxy.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">EnvoyProxy</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">custom-proxy-config</span><span class="w"> </span><span class="c"># &lt;-- mergeGateways: true</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">envoy-gateway-system</span><span class="w">
</span></span></span></code></pre></div><p>Toute <code>Gateway</code> déclarée avec <code>gatewayClassName: eg</code> bénéficiera automatiquement du merge.</p>
<h3 id="la-gateway-principale">La <code>Gateway</code> principale</h3>
<p>La gateway &quot;centrale&quot; reste dans <code>envoy-gateway-system</code>, avec ses listeners HTTP/HTTPS habituels :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># gateway-eg.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">envoy-gateway-system</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">gatewayClassName</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">listeners</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">http</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">HTTP</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">allowedRoutes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span><span class="nt">namespaces</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">          </span><span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="l">All</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPS</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">      </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">443</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">      </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">        </span><span class="nt">mode</span><span class="p">:</span><span class="w"> </span><span class="l">Terminate</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">        </span><span class="nt">certificateRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">          </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Secret</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ip-cert-tls</span><span class="w"> </span><span class="c"># Voir article précédent</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">      </span><span class="nt">allowedRoutes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">        </span><span class="nt">namespaces</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">          </span><span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="l">All</span><span class="w">
</span></span></span></code></pre></div><h2 id="exemple--app1-avec-sa-propre-gateway">Exemple : App1 avec sa propre Gateway</h2>
<p>Avec <code>mergeGateways</code>, chaque application peut avoir <strong>sa propre <code>Gateway</code></strong> dans son propre namespace.</p>
<h3 id="la-gateway-applicative">La Gateway applicative</h3>
<p>App1 déclare sa <code>Gateway</code> dans son namespace <code>app1</code>, avec son listener HTTPS et son certificat Let's Encrypt géré par cert-manager via l'annotation :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># app1-gateway.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app1-gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">app1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">cert-manager.io/cluster-issuer</span><span class="p">:</span><span class="w"> </span><span class="l">letsencrypt-prod-eg</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">gatewayClassName</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">listeners</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPS</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span><span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">app1.gravitek.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">      </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">443</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">      </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">        </span><span class="nt">mode</span><span class="p">:</span><span class="w"> </span><span class="l">Terminate</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">        </span><span class="nt">certificateRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Secret</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app1.gravitek.io-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span><span class="nt">allowedRoutes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">        </span><span class="nt">namespaces</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">          </span><span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="l">Same</span><span class="w">
</span></span></span></code></pre></div><h3 id="la-httproute">La HTTPRoute</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># app1-httproute.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPRoute</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">app1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">parentRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app1-gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">app1</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">sectionName</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="nt">hostnames</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span>- <span class="l">app1.gravitek.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span>- <span class="nt">matches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">        </span>- <span class="nt">path</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">PathPrefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">            </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span><span class="nt">backendRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">        </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app1</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">          </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">          </span><span class="nt">weight</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span></code></pre></div><p>Résultat : App1 est accessible via la <strong>même IP publique</strong> que toutes les autres applications, sans LB supplémentaire, avec son propre certificat géré de façon autonome dans son namespace.</p>
<h3 id="remarques">Remarques</h3>
<ul>
<li>
<p>Si besoin particulier, chaque application peut maintenant gérer des règles différentes sur les <code>ClientTrafficPolicy</code>, <code>BackendTrafficPolicy</code> &amp; co.</p>
</li>
<li>
<p>⚠️ <strong>Pour la gestion TLS</strong>, dans mon cas d'usage toutes les applis ont des routes HTTPS, pas HTTP. La redirection HTTP vers HTTPS est gérée par la <code>Gateway</code> par défaut, avec un redirect permanent. Cela m'a posé soucis avec cert-manager, et cette configuration marche bien chez moi.</p>
</li>
</ul>
<h2 id="alternative--listenersets">Alternative : ListenerSets</h2>
<p>Gateway API v1.3 propose une approche alternative avec les <a href="https://gateway-api.sigs.k8s.io/geps/gep-1713/"><code>ListenerSets</code></a> : une <code>Gateway</code> principale expose des listeners de base, et des <code>ListenerSet</code> permettent d'en ajouter depuis d'autres namespaces.</p>
<p>C'est séduisant car c'est <strong>natif Gateway API</strong> (pas une extension Envoy), avec un modèle de permissions plus granulaire sur les listeners. Mais il y a des limites importantes à date :</p>
<ul>
<li><strong>Seule <code>HTTPRoute</code> supportée</strong> : si vous utilisez des <code>GRPCRoute</code>, <code>TCPRoute</code>, elles ne peuvent pas référencer des listeners issus d'un <code>ListenerSet</code>.</li>
<li><strong>Encore expérimental</strong> : la feature est en alpha dans Gateway API, le support varie selon les implémentations.</li>
</ul>
<p>Pour l'instant, <code>mergeGateways</code> reste <strong>la solution que j'ai choisie</strong>, à vous de voir laquelle vous préférez.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Avec <code>mergeGateways</code>, on passe d'un modèle &quot;un LB par Gateway&quot; à &quot;un LB pour tout le cluster&quot;, retrouvant <em>à peu près</em> la configuration que j'avais avec Ingress NGINX :</p>
<ul>
<li>💰 <strong>Un seul LoadBalancer</strong> partagé entre toutes les Gateways</li>
<li>🌐 <strong>Une seule IP publique</strong> → gestion DNS simplifiée</li>
<li>🧩 <strong>Autonomie conservée</strong> : chaque application gère sa propre <code>Gateway</code> dans son namespace, avec ses propres certificats et listeners</li>
</ul>
<p>Bien sûr, rien ne vous empêche d'avoir plusieurs <code>Gateways</code> quand même, avec ou sans <code>mergeGateways</code>, cela dépend de votre besoin et architecture !</p>
<h2 id="ressources">Ressources</h2>
<ul>
<li><strong>Envoy Gateway - mergeGateways</strong> : <a href="https://gateway.envoyproxy.io/docs/tasks/operations/deployment-mode/#merged-gateways-onto-a-single-envoyproxy-fleet">https://gateway.envoyproxy.io/docs/tasks/operations/deployment-mode/#merged-gateways-onto-a-single-envoyproxy-fleet</a></li>
<li><strong>Gateway API - ListenerSets (GEP-1713)</strong> : <a href="https://gateway-api.sigs.k8s.io/geps/gep-1713/">gateway-api.sigs.k8s.io/geps/gep-1713</a></li>
<li><strong>Envoy Gateway - EnvoyProxy API</strong> : <a href="https://gateway.envoyproxy.io/docs/api/extension_types/#envoyproxy">gateway.envoyproxy.io/docs/api/extension_types/#envoyproxy</a></li>
</ul>]]></content:encoded></item><item><title>Envoy Gateway : backend par défaut &amp; certificat IP</title><link>https://www.vrchr.fr/posts/2026/07/29/envoy-gateway-default-backend-ip-cert/</link><pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2026/07/29/envoy-gateway-default-backend-ip-cert/</guid><description>Servir une page de maintenance par défaut sur une Envoy Gateway, en HTTPS, même sans FQDN, grâce aux certificats Let&amp;#39;s Encrypt sur adresse IP via le profil shortlived.</description><content:encoded><![CDATA[<p>On reprend les articles sur Envoy Gateway ! Comment faire quand on accède directement à l'IP publique de notre cluster porté par une <code>Gateway</code>, ou qu'aucune <code>HTTPRoute</code> ne matche ? On va voir dans cet article comment servir une <strong>page de maintenance</strong>, la servir <strong>en HTTPS</strong> sur l'<strong>IP elle-même</strong>, grâce aux certificats Let's Encrypt sur adresse IP et au profil <code>shortlived</code>.</p>
<p>Imaginons une Envoy Gateway exposée sur l'IP publique <code>203.0.113.14</code>, et une application <code>maintenance</code> (le service <code>maintenance</code> dans le namespace <code>maintenance</code>, sur le port <code>8080</code>) qui sert une simple page &quot;site en maintenance&quot;. On va voir comment servir cette page par défaut, en HTTPS sur l'IP publique elle-même.</p>
<p>⚠️ Je ne détaille pas l'installation d'Envoy Gateway ni de cert-manager (tous deux via Helm), ni non plus le déploiement de l'app de maintenance (simple page statique), qu'on suppose déjà en place. Nous allons nous concentrer sur le <strong>routing par défaut</strong> et le <strong>certificat sur IP</strong>.</p>
<h2 id="une-réponse-https-par-défaut-moche">Une réponse HTTP(S) par défaut &quot;moche&quot;</h2>
<p>Quand on déclare des <code>HTTPRoute</code> avec des <code>hostnames</code> bien précis (<code>app1.gravitek.io</code>, <code>app2.gravitek.io</code>, etc.), tout le reste,  qui ne correspond à aucune route (typiquement un <code>curl https://203.0.113.14/</code>, un scan, ou un DNS pas encore basculé) tombe dans le vide. Avec en prime une erreur de certificat TLS.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl">$ curl https://203.0.113.14/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">curl: <span class="o">(</span>60<span class="o">)</span> SSL certificate problem: unable to get <span class="nb">local</span> issuer certificate
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">More details here: https://curl.se/docs/sslcerts.html
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">curl failed to verify the legitimacy of the server and therefore could not
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">establish a secure connection to it. To learn more about this situation and
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">how to fix it, please visit the web page mentioned above.
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">$ curl -k https://203.0.113.14/
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="m">404</span> page not found
</span></span></code></pre></div><p>On va voir ci-après comment résoudre 2 problèmes d'un coup :</p>
<ol>
<li><strong>Un backend par défaut</strong> : tout ce qui ne matche aucune route applicative atterrit sur une page de maintenance propre.</li>
<li><strong>Du HTTPS même sur l'IP</strong> : plus de gros warning de certificat invalide quand on accède à l'IP directement. Historiquement, impossible avec Let's Encrypt (que des domaines)... mais ça a changé 🎉.</li>
</ol>
<p>Pour le point 2, ça résoud notamment un &quot;problème&quot; quand le mail du RSSI arrive : <em>&quot;Bonjour, votre IP 203.0.113.14 présente un certificat invalide, merci de corriger 🙏&quot;</em>. Jusqu'à présent je lui répondais qu'on ne pouvait pas avoir un certificat sur une IP, mais c'est maintenant possible 😅.</p>
<h2 id="pré-requis--configuration-des-composants">Pré-requis : configuration des composants</h2>
<p>Pour activer le support de la Gateway API &amp; du Backend par défaut, deux paramètres doivent être activés dans les <code>values</code> des différents charts (si vous utilisez Helm, sinon je vous laisse chercher les équivalents niveau configuration) :</p>
<h3 id="cert-manager--support-de-la-gateway-api">cert-manager : support de la Gateway API</h3>
<p>cert-manager doit savoir résoudre un challenge ACME <strong>via une <code>HTTPRoute</code></strong> (et non un <code>Ingress</code>). C'est l'option <code>enableGatewayAPI</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># custom-values-cert-manager.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nt">cert-manager</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="c"># CRDs</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">crds</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="c"># Enable Gateway API Support</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">  </span><span class="nt">config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w">    </span><span class="nt">enableGatewayAPI</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><p>Sans ça, le solver <code>http01</code> de type <code>gatewayHTTPRoute</code> (qu'on verra plus bas) est tout simplement ignoré.</p>
<h3 id="envoy-gateway--activer-lapi-backend">Envoy Gateway : activer l'API <code>Backend</code></h3>
<p>Pour router vers un endpoint qui <strong>n'est pas un <code>Service</code> Kubernetes</strong> (ici une app dans un autre namespace, qu'on veut joindre par son FQDN interne), Envoy Gateway propose une ressource <code>Backend</code>. C'est une <strong>extension API</strong>, on l'active :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># custom-values-envoy.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c"># https://gateway.envoyproxy.io/v1.7/install/install-helm/#enable-backend-api</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nt">config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">envoyGateway</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="nt">extensionApis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">      </span><span class="nt">enableBackend</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><h2 id="la-gateway-et-son-ip-publique">La Gateway et son IP publique</h2>
<p>Côté <code>GatewayClass</code>, rien de particulier : une classe standard rattachée au contrôleur Envoy Gateway.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># gatewayclass-eg.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">GatewayClass</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="nt">controllerName</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.envoyproxy.io/gatewayclass-controller</span><span class="w">
</span></span></span></code></pre></div><p>La <code>Gateway</code> elle-même expose deux listeners, HTTP et HTTPS, ce dernier terminant le TLS avec le secret <code>ip-cert-tls</code>, qu'on va générer juste après :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># gateway-eg.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">envoy-gateway-system</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">gatewayClassName</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">listeners</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">http</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">HTTP</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">allowedRoutes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span><span class="nt">namespaces</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">          </span><span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="l">All</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPS</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">      </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">443</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">      </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">        </span><span class="nt">mode</span><span class="p">:</span><span class="w"> </span><span class="l">Terminate</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">        </span><span class="nt">certificateRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">          </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">            </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Secret</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ip-cert-tls</span><span class="w"> </span><span class="c"># Certificat IP</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">      </span><span class="nt">allowedRoutes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">        </span><span class="nt">namespaces</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">          </span><span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="l">All</span><span class="w">
</span></span></span></code></pre></div><h2 id="un-certificat-lets-encrypt-sur-une-ip">Un certificat Let's Encrypt sur une IP</h2>
<p>Pendant des années, Let's Encrypt ne signait que des <strong>noms de domaine</strong>. Taper <code>https://203.0.113.14/</code> donnait un certificat invalide. Depuis <a href="https://letsencrypt.org/2025/07/01/issuing-our-first-ip-address-certificate/">l'annonce de Let's Encrypt sur les certificats pour adresses IP</a>, c'est devenu possible (c'est désormais en <a href="https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability/">GA (janvier 2026)</a>) avec <strong>une contrainte</strong> : ces certificats ne sont délivrés que via le <strong>profil <code>shortlived</code></strong>, des certificats à durée de vie très courte (~6 jours).</p>
<h3 id="le-clusterissuer-en-profil-shortlived">Le ClusterIssuer en profil <code>shortlived</code></h3>
<p>cert-manager supporte la sélection de ce <a href="https://cert-manager.io/docs/configuration/acme/#acme-profiles">profil ACME</a> via le champ <code>profile</code>. On déclare un <code>ClusterIssuer</code> dédié, qui résout le challenge HTTP-01 <strong>via une <code>HTTPRoute</code></strong> rattachée à notre gateway :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># clusterissuer-letsencrypt-prod-eg-shortlived.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIssuer</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">letsencrypt-prod-shortlived</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">acme</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">email</span><span class="p">:</span><span class="w"> </span><span class="l">remi@gravitek.io</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">server</span><span class="p">:</span><span class="w"> </span><span class="l">https://acme-v02.api.letsencrypt.org/directory</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">privateKeySecretRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">letsencrypt-prod-shortlived</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">profile</span><span class="p">:</span><span class="w"> </span><span class="l">shortlived</span><span class="w"> </span><span class="c"># Profil ACME</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">solvers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span>- <span class="nt">http01</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">          </span><span class="nt">gatewayHTTPRoute</span><span class="p">:</span><span class="w"> </span><span class="c"># Ref à ma Gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">            </span><span class="nt">parentRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">              </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">                </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">envoy-gateway-system</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">                </span><span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">                </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Gateway</span><span class="w">
</span></span></span></code></pre></div><p>Deux points à noter :</p>
<ul>
<li><strong><code>profile: shortlived</code></strong> : c'est <em>obligatoire</em> pour obtenir un certificat sur IP. Un profil classique refusera la demande.</li>
<li><strong><code>solvers.http01.gatewayHTTPRoute</code></strong> : cert-manager crée automatiquement une <code>HTTPRoute</code> temporaire sur la gateway <code>eg</code> le temps de résoudre le challenge <code>/.well-known/acme-challenge/...</code>. Pour un cert sur IP, seul HTTP-01 est possible : pas de DNS-01 (pas de zone DNS pour une IP, CQFD 🤷).</li>
</ul>
<h3 id="le-certificate-qui-demande-lip">Le Certificate qui demande l'IP</h3>
<p>Maintenant qu'on a notre Gateway, Route et ClusterIssuer, on peut déclarer notre certificat. Il ressemble à n'importe quel autre, sauf qu'on remplace <code>dnsNames</code> par <strong><code>ipAddresses</code></strong> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># certificate-ip.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Certificate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ip-cert</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">envoy-gateway-system</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">ip-cert-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">secretTemplate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">x509-certificate-exporter/exclude</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">issuerRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">letsencrypt-prod-shortlived</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIssuer</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="nt">ipAddresses</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span>- <span class="m">203.0.113.14</span><span class="w">
</span></span></span></code></pre></div><p>L'IP doit être l'IP publique de la gateway (celle exposée par le <code>Service</code> <code>LoadBalancer</code> d'Envoy chez moi), et le <code>secretName</code> doit correspondre au <code>certificateRefs</code> du listener HTTPS.</p>
<blockquote>
<p>💡 <strong>Pourquoi Le label <code>x509-certificate-exporter/exclude: &quot;true&quot;</code> ?</strong> Si vous monitorez l'expiration de vos certificats avec <a href="https://github.com/enix/x509-certificate-exporter"><code>x509-certificate-exporter</code></a>, un certificat qui expire tous les ~6 jours va générer une alerte en permanence. Je décide de le dégager de la supervision.</p>
</blockquote>
<p>⚠️ Les certificats <code>shortlived</code> durent quelques jours seulement : cert-manager les renouvelle très fréquemment, attention au rate-limit Let's Encrypt et au solver HTTP-01.</p>
<p>Une fois le certificat émis :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl -n envoy-gateway-system get certificate ip-cert
</span></span><span class="line"><span class="ln">2</span><span class="cl">NAME         READY   SECRET           AGE
</span></span><span class="line"><span class="ln">3</span><span class="cl">ip-cert      True    ip-cert-tls      2m
</span></span></code></pre></div><p>Et <code>curl https://203.0.113.14/</code> ne hurle plus au certificat invalide 🎉.</p>
<h2 id="le-backend-par-défaut-mais-avec-la-page-de-maintenance">Le backend par défaut, mais avec la page de maintenance</h2>
<p>On a maintenant un certificat valide pour l'IP, mais on a toujours une page par défaut moche. On va améliorer cela avec un backend par défaut.</p>
<h3 id="la-ressource-backend">La ressource <code>Backend</code></h3>
<p>L'app de maintenance vit dans son propre namespace <code>maintenance</code>. Plutôt que de jouer avec un <code>Service</code> cross-namespace et une <code>ReferenceGrant</code>, on déclare un <code>Backend</code> Envoy Gateway qui pointe vers le <strong>FQDN interne</strong> du service :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># backend-maintenance.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.envoyproxy.io/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Backend</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">maintenance</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">envoy-gateway-system</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">endpoints</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="nt">fqdn</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">maintenance.maintenance.svc.cluster.local</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span></code></pre></div><p>C'est exactement le cas d'usage de l'API <code>Backend</code> qu'on a activée plus haut : un endpoint arbitraire (FQDN, IP...) consommable depuis une <code>HTTPRoute</code>, dans le namespace de la gateway.</p>
<h3 id="la-httproute-par-défaut-catch-all">La HTTPRoute par défaut (catch-all)</h3>
<p>Le &quot;backend par défaut&quot; est simplement <strong>une <code>HTTPRoute</code> sans <code>hostnames</code></strong>. Sans hostname déclaré, elle matche <em>tout</em> le trafic du listener HTTPS qui n'est pas capté par une route applicative plus spécifique :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># httproute-default-backend.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPRoute</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default-backend</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">envoy-gateway-system</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">parentRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">sectionName</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span>- <span class="nt">backendRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.envoyproxy.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">          </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Backend</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">maintenance</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">          </span><span class="nt">weight</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">      </span><span class="nt">matches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">        </span>- <span class="nt">path</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">PathPrefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">            </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span></code></pre></div><p>Le <code>backendRefs</code> ne pointe pas un <code>Service</code> mais bien un <code>kind: Backend</code> du groupe <code>gateway.envoyproxy.io</code>. Les <code>HTTPRoute</code> applicatives, elles, ont des <code>hostnames</code> précis et sont donc <strong>plus spécifiques</strong> : Gateway API les fait gagner sur le catch-all. Notre <code>default-backend</code> ne capte donc que le reste IP directe, host inconnu, DNS pas encore basculé.</p>
<blockquote>
<p>⚠️ Je vous invite à lire la <a href="https://gateway.envoyproxy.io/docs/tasks/traffic/backend/">doc Envoy Gateway</a>, quelques warnings &amp; restrictions existent.</p>
</blockquote>
<h3 id="redirection-ip-http-vers-https">Redirection &quot;IP&quot; HTTP vers HTTPS</h3>
<p>Tant qu'on y est, on évite de servir la maintenance en clair sur le port 80 : on redirige tout HTTP vers HTTPS, là aussi via une route catch-all (sans hostname) attachée au listener <code>http</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># httproute-https-redirect.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPRoute</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">http-to-https-redirect</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">envoy-gateway-system</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">parentRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">sectionName</span><span class="p">:</span><span class="w"> </span><span class="l">http</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span>- <span class="nt">filters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">RequestRedirect</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">          </span><span class="nt">requestRedirect</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">            </span><span class="nt">scheme</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">            </span><span class="nt">statusCode</span><span class="p">:</span><span class="w"> </span><span class="m">301</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">      </span><span class="nt">matches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">        </span>- <span class="nt">path</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">PathPrefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">            </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span></code></pre></div><p>⚠️ Attention à l'ordre des opérations avec le challenge ACME : le solver HTTP-01 a besoin de répondre en <strong>HTTP</strong> sur <code>/.well-known/acme-challenge/</code>. La <code>HTTPRoute</code> temporaire créée par cert-manager est plus spécifique (elle matche ce path précis) que notre redirect catch-all <code>/</code>, donc le challenge passe avant la redirection, mais faites attention on sait jamais.</p>
<h2 id="le-résultat">Le résultat</h2>
<p>Au final, n'importe quel accès qui ne tombe pas sur une app connue :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Accès direct par IP, en HTTP → redirigé en HTTPS</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ curl -IL http://203.0.113.14/
</span></span><span class="line"><span class="ln">3</span><span class="cl">HTTP/1.1 <span class="m">301</span> Moved Permanently
</span></span><span class="line"><span class="ln">4</span><span class="cl">location: https://203.0.113.14/
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># ... puis servi par la page de maintenance, certificat valide</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">$ curl https://203.0.113.14/
</span></span><span class="line"><span class="ln">8</span><span class="cl">&lt;html&gt;&lt;body&gt;&lt;h1&gt;Page non trouvée.&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;
</span></span></code></pre></div><p>Plus de <code>404 page not found</code> moche, plus de warning de certificat : un point d'entrée propre, en HTTPS, même sur l'IP nue.</p>
<p>Voici &quot;en joli&quot; ce que ça peut donner:</p>
<p><img alt="Page not found" class="zoomable" decoding="async" loading="lazy" src="/2026/07/2026-07-29-page-not-found.webp"></p>
<h2 id="conclusion">Conclusion</h2>
<p>En assemblant quelques ressources, on obtient un comportement par défaut soigné sur la gateway :</p>
<ul>
<li>🧭 Un <strong>backend par défaut</strong> via une <code>HTTPRoute</code> sans hostname + une ressource <code>Backend</code> Envoy Gateway, qui rattrape tout le trafic non routé.</li>
<li>🔒 Du <strong>HTTPS valide sur l'IP elle-même</strong>, grâce aux certificats Let's Encrypt sur adresse IP et au profil <code>shortlived</code>.</li>
<li>🤖 Le tout <strong>auto-renouvelé</strong> par cert-manager en solver HTTP-01, même si le certificat est court.</li>
</ul>
<p>Avec tout ça, notre RSSI est content 😎. Ah, et pour les curieux, je n'ai pas testé en IPv6 !</p>
<h2 id="ressources">Ressources</h2>
<ul>
<li><strong>Let's Encrypt - Premier certificat sur adresse IP</strong> : <a href="https://letsencrypt.org/2025/07/01/issuing-our-first-ip-address-certificate/">letsencrypt.org/2025/07/01/issuing-our-first-ip-address-certificate</a></li>
<li><strong>Let's Encrypt - Certificats short-lived</strong> : <a href="https://letsencrypt.org/2025/02/20/first-short-lived-cert-issued/">letsencrypt.org/2025/02/20/first-short-lived-cert-issued</a></li>
<li><strong>Let's Encrypt - Disponibilité générale (6 jours &amp; IP)</strong> : <a href="https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability/">letsencrypt.org/2026/01/15/6day-and-ip-general-availability</a></li>
<li><strong>cert-manager - ACME profiles</strong> : <a href="https://cert-manager.io/docs/configuration/acme/#acme-profiles">cert-manager.io/docs/configuration/acme/#acme-profiles</a></li>
<li><strong>Envoy Gateway - Backend</strong> : <a href="https://gateway.envoyproxy.io/docs/tasks/traffic/backend/">gateway.envoyproxy.io/docs/tasks/traffic/backend</a></li>
</ul>]]></content:encoded></item><item><title>Envoy Gateway : OIDC, JWT &amp; Authorization</title><link>https://www.vrchr.fr/posts/2026/05/18/envoy-gateway-oidc-jwt-authz/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2026/05/18/envoy-gateway-oidc-jwt-authz/</guid><description>Protéger une application avec une SecurityPolicy OIDC via Envoy Gateway, et restreindre l&amp;#39;accès à un groupe spécifique avec JWT et authorization.</description><content:encoded><![CDATA[<p>Dans la continuité de l'article sur la <a href="../gateway-api-backendtls-trust-manager/">Gateway API</a>, une des ressources <a href="https://gateway.envoyproxy.io/">Envoy Gateway</a> avec laquelle j'ai joué est la <code>SecurityPolicy</code>. Depuis 1 seule ressource YAML, on peut brancher du vrai OIDC sur n'importe quelle <code>HTTPRoute</code> — sans déployer un <code>oauth2-proxy</code> à côté. Voici un exemple d'authentification avec Keycloak, et comment ajouter une notion d'authorization via filtrage par groupe.</p>
<p>Pour l'exemple, on va imaginer une app <code>mysecureapp</code> dans le namespace <code>mysecurens</code>, accessible sur <code>mysecureapp.gravitek.io</code>, et un Keycloak sur <code>sso.gravitek.io</code>.</p>
<p>💡 J'ai profité de cet article pour tester ma démo sur l'environnement Clever Cloud avec un cluster Kubernetes <a href="https://www.clever.cloud/developers/doc/kubernetes/">CKE</a> et un add-on <a href="https://www.clever.cloud/developers/doc/addons/keycloak/">Keycloak</a>. Je ne détaille pas le setup ici, peut-être dans un autre article 😉.</p>
<p>⚠️ Je n'explique pas non plus comment configurer Keycloak, ni comment exposer l'app via Gateway API, on suppose que vous avez déjà à disposition ces éléments.</p>
<h2 id="contexte--exposer-lapp-via-gateway-api">Contexte : exposer l'app via Gateway API</h2>
<h3 id="la-route-applicative-">La route applicative <code>/</code></h3>
<p>La <code>HTTPRoute</code> de base est classique : on route tout le trafic de <code>mysecureapp.gravitek.io</code> vers le service applicatif.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPRoute</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">parentRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">sectionName</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-https</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">hostnames</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span>- <span class="l">mysecureapp.gravitek.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span>- <span class="nt">matches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span>- <span class="nt">path</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">PathPrefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">            </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">      </span><span class="nt">backendRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">          </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span></code></pre></div><h3 id="la-route-oauth2-pour-le-callback-oidc">La route <code>/oauth2</code> pour le callback OIDC</h3>
<p>Pour que l'authentification OIDC fonctionne, la <code>redirectURL</code> et le <code>logoutPath</code> de la <code>SecurityPolicy</code> (qu'on verra juste après) doivent matcher une règle de la <code>HTTPRoute</code>. C'est mentionné dans la <a href="https://gateway.envoyproxy.io/docs/tasks/security/oidc/">doc Envoy Gateway</a>, en particulier la section <em>&quot;OIDC Authentication for a HTTPRoute&quot;</em> → <em>&quot;Create a SecurityPolicy&quot;</em>. C'est aussi valable pour une policy attachée à une Gateway.</p>
<p>Ici, la route <code>/</code> ci-dessus couvre déjà <code>/oauth2/callback</code> — donc en théorie une règle dédiée n'est pas obligatoire. Mais dès qu'on remplace ce <code>/</code> par des préfixes spécifiques (ex: <code>/myapp</code>, <code>/metrics</code>), aucun ne couvre <code>/oauth2</code> et la règle devient <strong>indispensable</strong>. Pour ne pas se mélanger les pinceaux, autant la déclarer explicitement dès le départ :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">  </span><span class="c"># Endpoint interne du filtre OAuth2</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span>- <span class="nt">matches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">        </span>- <span class="nt">path</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">PathPrefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">            </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">/oauth2</span><span class="w">
</span></span></span></code></pre></div><p>Pas besoin de <code>backendRefs</code> ici : ce path est entièrement géré en interne par le filtre OIDC d'Envoy (callback, échange de token, logout). Il faut juste que la <code>HTTPRoute</code> accepte de le router.</p>
<h2 id="oidc-avec-securitypolicy">OIDC avec SecurityPolicy</h2>
<p>Avant, avec ingress-nginx, ce type d'app tournait avec un <code>oauth2-proxy</code> déployé à côté, avec ses propres <code>Ingress</code>, sa conf, et son <code>Deployment</code> à maintenir. Avec Envoy Gateway, la <code>SecurityPolicy</code> fait exactement le même travail, sans déploiement supplémentaire :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># SecurityPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.envoyproxy.io/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">SecurityPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-oidc</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">targetRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPRoute</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">oidc</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">provider</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span><span class="nt">issuer</span><span class="p">:</span><span class="w"> </span><span class="l">https://sso.gravitek.io/realms/myrealm</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">clientID</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">clientSecret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-client-secret  </span><span class="w"> </span><span class="c"># Secret K8s contenant le client_secret</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nt">redirectURL</span><span class="p">:</span><span class="w"> </span><span class="l">https://mysecureapp.gravitek.io/oauth2/callback</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">logoutPath</span><span class="p">:</span><span class="w"> </span><span class="l">/oauth2/logout</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">scopes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span>- <span class="l">openid</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">      </span>- <span class="l">groups </span><span class="w"> </span><span class="c"># On se basera plus tard la-dessus pour l&#39;authz</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">    </span><span class="nt">cookieNames</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">      </span><span class="nt">idToken</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;IdTokenMySecureApp&#34;</span><span class="w">
</span></span></span></code></pre></div><p>Le <code>targetRefs</code> pointe sur la <code>HTTPRoute</code>. Envoy intercepte toutes les requêtes, lance le flow OAuth2/OIDC si l'utilisateur n'est pas authentifié, et pose le cookie <code>IdTokenMySecureApp</code> avec l'id_token JWT une fois connecté.</p>
<p>➡️ Résultat : toute personne avec un compte valide dans le realm peut accéder à l'app. C'est déjà pas mal... mais on peut mieux faire !</p>
<blockquote>
<p>💡 <strong>Pourquoi fixer <code>cookieNames.idToken</code> ?</strong> Ce champ est optionnel : sans lui, Envoy nomme le cookie <code>IdToken</code> mais y ajoute un suffixe de hash dérivé du domaine (ex: <code>IdToken-5671b67c</code>), donc imprévisible. Or le filtre JWT relira ce cookie plus tard via <code>extractFrom.cookies</code> et a besoin d'un nom stable. On l'épingle donc explicitement.</p>
</blockquote>
<h2 id="authn--authz">Authn ≠ Authz</h2>
<p>L'OIDC, c'est de l'<strong>authentification</strong> : il vérifie <em>qui vous êtes</em>. Il ne dit rien sur <em>ce que vous avez le droit de faire</em>. Avec la SecurityPolicy ci-dessus, n'importe quel compte du realm peut se connecter 🙈.</p>
<p>Il faut alors de l'<strong>autorisation</strong> : restreindre ici l'accès aux membres d'un groupe spécifique. Envoy Gateway résout ça avec deux blocs supplémentaires dans la même <code>SecurityPolicy</code> :</p>
<ul>
<li><strong><code>jwt</code></strong> : pour extraire et valider le token depuis le cookie posé par le filtre OIDC</li>
<li><strong><code>authorization</code></strong> : pour définir les règles d'accès à partir des claims du token</li>
</ul>
<p>L'ordre des filtres générés par Envoy Gateway est <code>oauth2</code> (OIDC) → <code>jwt</code> → <code>authorization</code>, le filtre OIDC s'exécute en premier. Une requête non authentifiée est donc interceptée et redirigée vers Keycloak <em>avant</em> que le filtre JWT ne soit évalué : ce dernier ne voit que des requêtes déjà authentifiées, qui portent l'<em>id_token</em> dans le cookie.</p>
<p><img alt="Ordre des filtres Envoy : OIDC → JWT → Authorization" class="zoomable" decoding="async" loading="lazy" src="/2026/05/2026-05-18-filter-order.svg"></p>
<h2 id="filtrage-par-groupe-avec-jwt-et-authorization">Filtrage par groupe avec JWT et authorization</h2>
<p>Pour pouvoir vérifier les groupes d'un utilisateur, on va d'abord configurer le filtre JWT pour lire le cookie <code>IdTokenMySecureApp</code> et en extraire le claim <code>groups</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># SecurityPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">jwt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">providers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">keycloak</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">        </span><span class="nt">issuer</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://sso.gravitek.io/realms/myrealm&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">        </span><span class="nt">remoteJWKS</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">          </span><span class="nt">cacheDuration</span><span class="p">:</span><span class="w"> </span><span class="l">300s</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">          </span><span class="nt">uri</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://sso.gravitek.io/realms/myrealm/protocol/openid-connect/certs&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">extractFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">          </span><span class="nt">cookies</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">            </span>- <span class="l">IdTokenMySecureApp</span><span class="w">
</span></span></span></code></pre></div><p>Ensuite, on configure le bloc <code>authorization</code> où par défaut tout est <strong>refusé</strong>, et on autorise uniquement les membres du groupe <code>/admins</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="w">  </span><span class="nt">authorization</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="nt">defaultAction</span><span class="p">:</span><span class="w"> </span><span class="l">Deny</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">allow-admin-group</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">Allow</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">        </span><span class="nt">principal</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">          </span><span class="nt">jwt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">            </span><span class="nt">provider</span><span class="p">:</span><span class="w"> </span><span class="l">keycloak</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">            </span><span class="nt">claims</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">              </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">groups</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">                </span><span class="nt">valueType</span><span class="p">:</span><span class="w"> </span><span class="l">StringArray</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">                </span><span class="nt">values</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/admins&#34;</span><span class="p">]</span><span class="w">
</span></span></span></code></pre></div><p>Le claim <code>groups</code> est un tableau dans Keycloak, d'où le <code>valueType: StringArray</code>. Envoy vérifie que <code>/admins</code> est présent dans ce tableau — si oui, la requête passe. Sinon, <code>403</code>.</p>
<h3 id="groupes-keycloak">Groupes Keycloak</h3>
<p>Pour que le claim <code>groups</code> reflète l'appartenance aux groupes, il faut un mapper de type <strong>Group Membership</strong> sur le client scope.</p>
<p><img alt="Group Membership mapper" class="zoomable" decoding="async" loading="lazy" src="/2026/05/2026-05-18-keycloak-group-membership-mapper.webp"></p>
<p>Avec l'option <em>Full group path</em> <strong>activée</strong> sur le mapper Group Membership, le claim vaut <code>/admins</code> (avec le slash initial) ; désactivée, il vaut <code>admins</code>. Envoy fait une comparaison <strong>exacte</strong> : <code>values: [&quot;/admins&quot;]</code> ne matchera <em>jamais</em> un claim <code>admins</code>, et inversement. Un simple <code>/</code> de différence = <code>403</code>. Choisissez une convention et gardez la même des deux côtés (le mapper Keycloak et le bloc <code>authorization</code>).</p>
<p><img alt="Full group path" class="zoomable" decoding="async" loading="lazy" src="/2026/05/2026-05-18-keycloak-full-group-path.webp"></p>
<h2 id="securitypolicy-complète">SecurityPolicy complète</h2>
<p>En assemblant le tout, un seul objet qui fait l'authn et l'authz :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.envoyproxy.io/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">SecurityPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-oidc</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">targetRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPRoute</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">oidc</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">provider</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span><span class="nt">issuer</span><span class="p">:</span><span class="w"> </span><span class="l">https://sso.gravitek.io/realms/myrealm</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">clientID</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">clientSecret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-client-secret</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nt">redirectURL</span><span class="p">:</span><span class="w"> </span><span class="l">https://mysecureapp.gravitek.io/oauth2/callback</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">logoutPath</span><span class="p">:</span><span class="w"> </span><span class="l">/oauth2/logout</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">scopes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span>- <span class="l">openid</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">      </span>- <span class="l">groups</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">    </span><span class="nt">cookieNames</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">      </span><span class="nt">idToken</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;IdTokenMySecureApp&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">  </span><span class="nt">jwt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">    </span><span class="nt">providers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">keycloak</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">        </span><span class="nt">issuer</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://sso.gravitek.io/realms/myrealm&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">        </span><span class="nt">remoteJWKS</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">          </span><span class="nt">cacheDuration</span><span class="p">:</span><span class="w"> </span><span class="l">300s</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">          </span><span class="nt">uri</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://sso.gravitek.io/realms/myrealm/protocol/openid-connect/certs&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">        </span><span class="nt">extractFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">          </span><span class="nt">cookies</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">            </span>- <span class="l">IdTokenMySecureApp</span><span class="w">
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="w">  </span><span class="nt">authorization</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="w">    </span><span class="nt">defaultAction</span><span class="p">:</span><span class="w"> </span><span class="l">Deny</span><span class="w">
</span></span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">allow-admin-group</span><span class="w">
</span></span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">Allow</span><span class="w">
</span></span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="w">        </span><span class="nt">principal</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="w">          </span><span class="nt">jwt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">44</span><span class="cl"><span class="w">            </span><span class="nt">provider</span><span class="p">:</span><span class="w"> </span><span class="l">keycloak</span><span class="w">
</span></span></span><span class="line"><span class="ln">45</span><span class="cl"><span class="w">            </span><span class="nt">claims</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">46</span><span class="cl"><span class="w">              </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">groups</span><span class="w">
</span></span></span><span class="line"><span class="ln">47</span><span class="cl"><span class="w">                </span><span class="nt">valueType</span><span class="p">:</span><span class="w"> </span><span class="l">StringArray</span><span class="w">
</span></span></span><span class="line"><span class="ln">48</span><span class="cl"><span class="w">                </span><span class="nt">values</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/admins&#34;</span><span class="p">]</span><span class="w">
</span></span></span></code></pre></div><p>Le diagramme ci-dessous résume le flux complet :</p>
<p><img alt="Flux d'authentification OIDC + JWT + Authorization" class="zoomable" decoding="async" loading="lazy" src="/2026/05/2026-05-18-oidc-auth-flow.svg"></p>
<p>Et donc, après tout ce paramétrage, vous aurez un beau message si vous n'avez pas les droits nécessaires 😕 :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">RBAC: access denied
</span></span></code></pre></div><p>Sinon, si vous êtes dans le bon groupe, vous pourrez accéder à votre application 🥳 !</p>
<h2 id="allons-plus-loin--passer-lidentité-à-lapplication">Allons plus loin : passer l'identité à l'application</h2>
<p>Tout ce qu'on a vu jusqu'ici se passe <strong>dans la gateway</strong> : la décision authn/authz est prise par Envoy, et le backend <code>mysecureapp</code> ne voit jamais le token ni les groupes. C'est suffisant pour protéger l'accès, mais parfois l'app a <em>elle-même</em> besoin de savoir qui est connecté (afficher le nom de l'utilisateur, adapter l'UI selon les groupes, appeler une autre API en son nom...).</p>
<p>Deux champs optionnels permettent de transmettre cette info au backend.</p>
<h3 id="forwardaccesstoken--laccess-token-vers-le-backend"><code>forwardAccessToken</code> — l'access token vers le backend</h3>
<p>Dans le bloc <code>oidc</code>, <code>forwardAccessToken: true</code> demande à Envoy de transmettre l'<strong>access token</strong> OIDC au backend (dans le header <code>Authorization</code>) :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">  </span><span class="nt">oidc</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="c"># ...</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">forwardAccessToken</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><h3 id="claimtoheaders--un-claim-dans-un-header"><code>claimToHeaders</code> — un claim dans un header</h3>
<p>Dans le bloc <code>jwt</code>, <code>claimToHeaders</code> injecte la valeur d'un claim du token dans un header HTTP transmis au backend :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">  </span><span class="nt">jwt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="nt">providers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">keycloak</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">        </span><span class="c"># ...</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">        </span><span class="nt">claimToHeaders</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">          </span>- <span class="nt">claim</span><span class="p">:</span><span class="w"> </span><span class="l">groups</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">            </span><span class="nt">header</span><span class="p">:</span><span class="w"> </span><span class="l">x-user-groups</span><span class="w">
</span></span></span></code></pre></div><p>Le nom du header (<code>x-user-groups</code>) est ici libre : ce n'est ni un standard ni une convention Keycloak. L'app lira alors les groupes dans ce header sans avoir à décoder le JWT elle-même.</p>
<h3 id="valider-le-flux-avec-un-backend-echo">Valider le flux avec un backend &quot;echo&quot;</h3>
<p>Pour vérifier concrètement ce qu'Envoy injecte (cookie, <code>Authorization</code>, headers custom), on peut s'appuyer sur l'application <a href="https://github.com/mendhak/docker-http-https-echo"><code>mendhak/http-https-echo</code></a>. Avec la variable <code>JWT_HEADER</code>, il décode le <a href="https://hub.docker.com/r/mendhak/http-https-echo#decode-jwt-header">JWT</a> d'un header donné et l'ajoute, claims lisibles, dans sa réponse JSON.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w"> 
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w"> 
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">echo</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mendhak/http-https-echo</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">JWT_HEADER</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Authorization&#34;</span><span class="w">   </span><span class="c"># décode l&#39;access token</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w"> 
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w"> 
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w"> 
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">    </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">      </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span></code></pre></div><ul>
<li>L'<strong>id_token</strong> est dans le cookie <code>IdTokenMySecureApp</code> → l'echo l'affiche <strong>brut, non décodé</strong> (à passer dans <a href="https://jwt.io">jwt.io</a> à la main, ou via <code>jwt decode</code>).</li>
<li>L'<strong>access token</strong> n'arrive dans <code>Authorization: Bearer …</code> <em>que si</em> <code>forwardAccessToken: true</code> → là <code>JWT_HEADER=Authorization</code> le décode automatiquement. C'est le cas d'usage idéal pour valider <code>forwardAccessToken</code>.</li>
</ul>
<p>Une fois authentifié en tant que membre du groupe autorisé — et avec les deux options activées (<code>forwardAccessToken: true</code>, <code>claimToHeaders</code>) — l'echo renvoie ceci :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonc" data-lang="jsonc"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;path&#34;</span><span class="p">:</span> <span class="s2">&#34;/&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&#34;headers&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nt">&#34;host&#34;</span><span class="p">:</span> <span class="s2">&#34;mysecureapp.gravitek.io&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nt">&#34;x-forwarded-proto&#34;</span><span class="p">:</span> <span class="s2">&#34;https&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nt">&#34;x-request-id&#34;</span><span class="p">:</span> <span class="s2">&#34;02d9c605-3391-48d7-ba70-9b7f95f0c0c2&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="c1">// forwardAccessToken: true → l&#39;access token en Bearer
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nt">&#34;authorization&#34;</span><span class="p">:</span> <span class="s2">&#34;Bearer eyJhbGciOiJSUzI1NiIsInR5cCI…&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="c1">// posés par le filtre OIDC : access token + id_token
</span></span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nt">&#34;cookie&#34;</span><span class="p">:</span> <span class="s2">&#34;AccessToken-10c7e844=eyJhbGc…; IdTokenMySecureApp=eyJhbGc…&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="c1">// claimToHeaders : valeur du claim `groups`, encodée en base64
</span></span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nt">&#34;x-user-groups&#34;</span><span class="p">:</span> <span class="s2">&#34;WyIvYWRtaW5zIl0=&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="nt">&#34;method&#34;</span><span class="p">:</span> <span class="s2">&#34;GET&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="c1">// JWT_HEADER=Authorization → l&#39;access token décodé par l&#39;echo
</span></span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="nt">&#34;jwt&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="nt">&#34;payload&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">      <span class="nt">&#34;iss&#34;</span><span class="p">:</span> <span class="s2">&#34;https://sso.gravitek.io/realms/myrealm&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">      <span class="nt">&#34;aud&#34;</span><span class="p">:</span> <span class="s2">&#34;account&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">      <span class="nt">&#34;azp&#34;</span><span class="p">:</span> <span class="s2">&#34;mysecureapp&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">      <span class="nt">&#34;scope&#34;</span><span class="p">:</span> <span class="s2">&#34;openid groups&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">      <span class="nt">&#34;groups&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="s2">&#34;/admins&#34;</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Les trois mécanismes vus plus haut sont visibles d'un coup d'œil :</p>
<ul>
<li><strong><code>authorization: Bearer …</code></strong> → c'est <code>forwardAccessToken: true</code> qui l'a ajouté ; sans lui, ce header est absent.</li>
<li><strong><code>x-user-groups: WyIvYWRtaW5zIl0=</code></strong> → c'est <code>claimToHeaders</code> ; la valeur est le base64 du tableau JSON du claim (<code>echo WyIvYWRtaW5zIl0= | base64 -d</code> → <code>[&quot;/admins&quot;]</code>). Envoy n'envoie pas la valeur brute mais la représentation JSON encodée.</li>
<li><strong><code>jwt.payload.groups</code></strong> → l'echo a décodé l'access token (grâce à <code>JWT_HEADER=Authorization</code>) ; on y retrouve <code>groups</code>, c'est exactement le claim sur lequel la règle <code>authorization</code> a statué pour laisser passer la requête.</li>
</ul>
<p>Le cookie <code>IdTokenMySecureApp</code>, lui, contient l'id_token : c'est <em>lui</em> que le filtre JWT relit (via <code>extractFrom.cookies</code>) pour appliquer l'<code>authorization</code>, indépendamment de ce qui est transmis au backend.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Maintenant, avec une seule <code>SecurityPolicy</code>, on peut gérer l'<strong>authn OIDC, l'extraction JWT, l'authorization par claim</strong>, colocalisé avec la <code>HTTPRoute</code>. Plus de déploiement <code>oauth2-proxy</code>, plus de conf éparpillée. Plus simple, plus clair, j'aime bien !</p>
<p>Une fois le pattern en place, protéger une nouvelle app par groupe devient simple = un bloc <code>oidc</code>, un bloc <code>jwt</code>, une règle <code>authorization</code>, et go ! 🎉</p>
<p>Rendez-vous bientôt pour un prochain article autour d'Envoy Gateway et les Gateway API !</p>
<h2 id="ressources">Ressources</h2>
<ul>
<li><strong>Envoy Gateway - OIDC</strong> : <a href="https://gateway.envoyproxy.io/docs/tasks/security/oidc/">gateway.envoyproxy.io/docs/tasks/security/oidc</a> — inclut le warning sur le matching <code>redirectURL</code> / <code>HTTPRoute</code></li>
<li><strong>Envoy Gateway - JWT Authentication</strong> : <a href="https://gateway.envoyproxy.io/docs/tasks/security/jwt-authentication/">gateway.envoyproxy.io/docs/tasks/security/jwt-authentication</a></li>
<li><strong>Envoy Gateway - JWT Claim-Based Authorization</strong> : <a href="https://gateway.envoyproxy.io/docs/tasks/security/jwt-claim-authorization/">gateway.envoyproxy.io/docs/tasks/security/jwt-claim-authorization</a> — exemples avec <code>StringArray</code> et filtrage par claim</li>
</ul>]]></content:encoded></item><item><title>Gateway API, backend TLS auto-signé &amp; trust-manager</title><link>https://www.vrchr.fr/posts/2026/05/04/gateway-api-backendtls-trust-manager/</link><pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2026/05/04/gateway-api-backendtls-trust-manager/</guid><description>Comment gérer un backend HTTPS auto-signé avec Gateway API en gérant proprement la vérification du certificat avec trust-manager</description><content:encoded><![CDATA[<p>Dans le cadre de la migration de ressources Ingress vers <a href="https://gateway-api.sigs.k8s.io/">Gateway API</a>, j'ai dû gérer le cas d'un backend qui expose son service en <strong>HTTPS</strong> avec un certificat <strong>auto-signé</strong>. Avec <code>ingress-nginx</code> ça passait à coups d'annotations, sans trop se poser de questions. En Gateway API, c'est plus restrictif : il faut une vraie CA. Voici comment je m'en suis sorti proprement avec <strong>trust-manager</strong>.</p>
<p>Pour l'exemple, on va imaginer une app <code>mysecureapp</code> dans le namespace <code>mysecurens</code>, qui expose son service <code>mysecureapp</code> sur le port <code>8443</code> en HTTPS, avec un certificat auto-signé.</p>
<h2 id="avant--ingress-nginx--annotations">Avant : ingress-nginx &amp; annotations</h2>
<p>Côté <code>ingress-nginx</code>, c'était assez direct. Une ressource <code>Ingress</code> avec une seule annotation, et go :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/backend-protocol</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;HTTPS&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">ingressClassName</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp.gravitek.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">          </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">            </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">Prefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">            </span><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">              </span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">                </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">                </span><span class="nt">port</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">                  </span><span class="nt">number</span><span class="p">:</span><span class="w"> </span><span class="m">8443</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">    </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">        </span>- <span class="l">mysecureapp.gravitek.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">      </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp.gravitek.io-tls</span><span class="w"> </span><span class="c"># Certificat public Let&#39;s Encrypt</span><span class="w">
</span></span></span></code></pre></div><p>L'annotation <code>backend-protocol: &quot;HTTPS&quot;</code> est claire, elle indique à <code>nginx</code> de parler en TLS au backend. Et c'est tout. Par défaut, <code>ingress-nginx</code> <strong>ne vérifie pas</strong> le certificat du backend 🙈. Tant qu'on ne lui demande pas explicitement (<code>proxy-ssl-verify: &quot;on&quot;</code> + <code>proxy-ssl-secret</code>), il chiffre mais ne valide rien.</p>
<h2 id="après--gateway-api--backendtlspolicy">Après : Gateway API &amp; BackendTLSPolicy</h2>
<p>En Gateway API (j'utilise <a href="https://gateway.envoyproxy.io/">Envoy Gateway</a>, mais le principe est le même côté API), la route HTTP devient une <code>HTTPRoute</code>, et le TLS vers le backend se déclare avec une <a href="https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/"><code>BackendTLSPolicy</code></a> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># HTTPRoute</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPRoute</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">hostnames</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="l">mysecureapp.gravitek.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">parentRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w"> </span><span class="c"># Nom de ma gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w"> </span><span class="c"># Pour l&#39;exemple on a la GW dans le même NS que l&#39;appli</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span><span class="nt">sectionName</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-https</span><span class="w"> </span><span class="c"># Section de Gateway qui écoute en HTTPS</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span>- <span class="nt">backendRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">        </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8443</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">      </span><span class="nt">matches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">        </span>- <span class="nt">path</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">PathPrefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">            </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># BackendTLSPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">BackendTLSPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-backend-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">targetRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">validation</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp.mysecurens.svc.cluster.local</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">caCertificateRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ConfigMap</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca</span><span class="w"> </span><span class="c"># on va voir cela après ;)</span><span class="w">
</span></span></span></code></pre></div><p>Côté flux, ça donne :</p>
<p><img alt="Workflow Gateway API : du client au pod" class="zoomable" decoding="async" loading="lazy" src="/2026/05/2026-05-04-gateway-api-flow.svg"></p>
<p>C'est propre, c'est déclaratif, c'est versionné dans un objet bien identifié... mais 💥, contrairement à <code>nginx</code>, <strong>il n'y a pas de mode <code>verify: off</code></strong>. La spec impose soit une <code>caCertificateRefs</code>, soit un <code>wellKnownCACertificates: System</code>. Impossible de faire du <code>SkipVerify</code> (et c'est tant mieux).</p>
<p>Sauf que : mon backend a un certificat <strong>auto-signé</strong>, donc pas de CA &quot;publique&quot; à pointer. Et il faut bien matérialiser cette CA dans un <code>ConfigMap</code> du namespace <code>mysecurens</code> pour que la <code>BackendTLSPolicy</code> puisse la référencer. Comment faire ça proprement ? 🤔</p>
<h3 id="backendtlspolicy--deux-modes-de-validation">BackendTLSPolicy : deux modes de validation</h3>
<p>Avant d'aller plus loin, un mot sur les <a href="https://gateway-api.sigs.k8s.io/guides/tls/#upstream-tls">options possibles côté Gateway API</a> pour parler en TLS au backend. La spec <code>BackendTLSPolicy</code> définit deux modes de validation, mutuellement exclusifs :</p>
<ul>
<li><strong><code>wellKnownCACertificates: System</code></strong> : le gateway utilise le bundle de CA du système (Let's Encrypt, DigiCert et autres autorités publiques). Si mon backend expose un certificat signé par une CA publique — typiquement un service managé ou un endpoint SaaS — c'est l'option la plus simple : rien à gérer, rien à distribuer.</li>
<li><strong><code>caCertificateRefs</code></strong> : le gateway charge la (ou les) CA depuis un <code>ConfigMap</code> que je fournis dans le namespace de la <code>BackendTLSPolicy</code>. C'est le mode obligatoire dès qu'on a une <strong>CA privée</strong> : certificat auto-signé, PKI maison, etc.</li>
</ul>
<p>C'est ce <strong>deuxième cas</strong> qui motive cet article : un service interne avec certificat auto-signé, qu'on veut consommer en TLS validé. Si le backend utilise un certificat public, <code>wellKnownCACertificates: System</code> et c'est plié — pas besoin de trust-manager.</p>
<h3 id="cacertificaterefs--secret-ou-configmap-">caCertificateRefs : Secret ou ConfigMap ?</h3>
<p>Avec Envoy Gateway, on pourrait très bien utiliser un <code>Secret</code> comme <code>caCertificateRefs</code>, et donc éviter en partie la suite de cet article (merci @Shinro pour la remarque, cf issue <a href="https://github.com/envoyproxy/gateway/issues/2777">#2777</a>). Mais c'est un support <strong>Implementation-Specific</strong> au sens de la <a href="https://gateway-api.sigs.k8s.io/reference/spec/">spec Gateway API</a> — Envoy Gateway le permet, d'autres implémentations non.</p>
<p>Cependant, ça ne résoudrait pas un problème de rotation de <code>ca.crt</code>, par exemple. Continuons alors la lecture ! 😉</p>
<h2 id="trois-tentatives-pour-définir-la-ca-dans-un-configmap">Trois tentatives pour définir la CA dans un ConfigMap</h2>
<p>Parmi mes expérimentations, j'ai noté 3 possibilités permettant d'avoir la CA du certificat auto-signé de mon application.</p>
<h3 id="1-la-recopie-manuelle-du-cacrt">1. La recopie manuelle du <code>ca.crt</code></h3>
<p>La méthode &quot;quick &amp; dirty&quot; : récupérer le <code>ca.crt</code> du <code>Secret</code> source, et le re-créer en <code>ConfigMap</code> à la main :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl -n mysecurens get secret mysecureapp-internal-tls -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.data.ca\.crt}&#39;</span> <span class="p">|</span> base64 -d &gt; ca.crt
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ kubectl -n mysecurens create configmap mysecureapp-ca --from-file<span class="o">=</span>ca.crt
</span></span></code></pre></div><p>Ça marche... jusqu'à la <strong>première rotation de la CA</strong> : il faut repasser à la main, régénérer, recommiter, rolling-restart. Et si on oublie, c'est <code>BackendTLSPolicy</code> rouge et erreur 503 à la clé. ❌</p>
<p>Cette solution simple ne faisait que décaler le problème pour mon moi futur 😬.</p>
<h3 id="2-synchro-automatique-avec-kyverno">2. Synchro automatique avec Kyverno</h3>
<p>Pour automatiser, j'ai pensé à <a href="https://kyverno.io/">Kyverno</a> avec une <code>GeneratingPolicy</code> qui prend le <code>Secret</code> source et génère le <code>ConfigMap</code> cible :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">policies.kyverno.io/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">GeneratingPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sync-ca-to-configmap</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">matchConstraints</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">resourceRules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span>- <span class="nt">apiGroups</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">apiVersions</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;v1&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;secrets&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="nt">operations</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CREATE&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;UPDATE&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">        </span><span class="nt">resourceNames</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;mysecureapp-internal-tls&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">generate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span>- <span class="nt">expression</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="sd">        [...]</span><span class="w">
</span></span></span></code></pre></div><p>Sur le papier, ça paraît simple : le mode synchronisé était censé garder le <code>ConfigMap</code> aligné sur le <code>Secret</code> à chaque update. En vrai, <strong>chaque <code>UPDATE</code></strong> du secret source <strong>supprimait le <code>ConfigMap</code></strong> au lieu de le mettre à jour, avant de le recréer. Concrètement : une <code>BackendTLSPolicy</code> qui flappe à chaque renouvellement, donc pire qu'avant. ❌</p>
<p>Étant sur une version de kyverno assez &quot;vieille&quot; (1.15) avec une CR <code>v1alpha1</code>, je suspecte un problème de stabilité / maturité, sur les dernières versions en <code>v1</code> j'espère que ça devrait aller mieux.</p>
<p>Et donc comme pour le point précédent, ça pourrait le faire pour mon moi du futur qui fera la maj kyverno 😅.</p>
<h3 id="3-trust-manager--le-bon-outil-qui-juste-marche">3. trust-manager : le bon outil qui juste marche</h3>
<p>Et puis je suis tombé sur <a href="https://cert-manager.io/docs/trust/trust-manager/"><code>trust-manager</code></a>, l'outil <strong>officiel</strong> de l'écosystème <strong>cert-manager</strong>, dont le seul boulot est exactement ce qu'on cherche : prendre une CA en source et la <strong>distribuer</strong> sous forme de <strong><code>ConfigMap</code></strong> dans les namespaces ciblés, avec sync automatique en cas de rotation.</p>
<p>C'est cette troisième approche qu'on va retenir pour la suite.</p>
<h2 id="la-solution--cert-manager--trust-manager">La solution : cert-manager + trust-manager</h2>
<p>L'idée : émettre tous les certificats internes du cluster avec une <strong>CA interne</strong> gérée par <code>cert-manager</code>, et <strong>distribuer le <code>ca.crt</code></strong> dans les namespaces qui en ont besoin avec <code>trust-manager</code>.</p>
<p>Plutôt qu'une CA par appli (chacune dans son ns, à répliquer cross-namespace), on part sur <strong>une CA unique</strong> au niveau du cluster, qui signe tous les certificats feuilles internes. C'est un pattern PKI privée classique : une seule racine de confiance pour tout le trafic interne du cluster.</p>
<p><img alt="Architecture PKI interne avec cert-manager et trust-manager" class="zoomable" decoding="async" loading="lazy" src="/2026/05/2026-05-04-pki-trust-manager.svg"></p>
<h3 id="1-une-ca-interne-avec-cert-manager">1. Une CA interne avec cert-manager</h3>
<p>On bootstrap avec un <code>ClusterIssuer</code> self-signed, qui ne sert qu'à générer la CA racine, puis on déclare un <code>ClusterIssuer</code> qui signera tous les certificats feuilles :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># ClusterIssuer self-signed</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIssuer</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">selfsigned</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="nt">selfSigned</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># CA Interne</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Certificate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">isCA</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="c"># On définit comme CA interne / privée</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">commonName</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">duration</span><span class="p">:</span><span class="w"> </span><span class="l">87600h0m0s</span><span class="w"> </span><span class="c"># 10 ans</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">renewBefore</span><span class="p">:</span><span class="w"> </span><span class="l">2160h0m0s</span><span class="w"> </span><span class="c"># 90 jours</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">privateKey</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">algorithm</span><span class="p">:</span><span class="w"> </span><span class="l">ECDSA</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">size</span><span class="p">:</span><span class="w"> </span><span class="m">256</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">rotationPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Always</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">  </span><span class="nt">issuerRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">selfsigned</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIssuer</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># ClusterIssuer utilisant la CA interne</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIssuer</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca-issuer</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="nt">ca</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">    </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca-tls</span><span class="w">
</span></span></span></code></pre></div><p>À partir de là, <code>internal-ca-issuer</code> peut signer le certificat feuille de <code>mysecureapp</code> (et de tous les autres services internes du cluster). Côté <code>mysecureapp</code>, on remplace donc le certificat auto-signé par un certificat signé par cette CA interne :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Certificate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-internal-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-internal-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">duration</span><span class="p">:</span><span class="w"> </span><span class="l">2160h0m0s</span><span class="w"> </span><span class="c"># 90 jours</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">renewBefore</span><span class="p">:</span><span class="w"> </span><span class="l">360h0m0s</span><span class="w"> </span><span class="c"># 15 jours</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">privateKey</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">algorithm</span><span class="p">:</span><span class="w"> </span><span class="l">ECDSA</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">size</span><span class="p">:</span><span class="w"> </span><span class="m">256</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">rotationPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Always</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="nt">dnsNames</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="c"># Nom DNS interne pour joindre le service</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span>- <span class="l">mysecureapp.mysecurens.svc.cluster.local</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span>- <span class="l">mysecureapp.mysecurens.svc</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span>- <span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">  </span><span class="nt">issuerRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca-issuer</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIssuer</span><span class="w">
</span></span></span></code></pre></div><p>⚠️ Point d'attention : le <code>dnsNames</code> doit contenir le <code>hostname</code> validé par la <code>BackendTLSPolicy</code> (ici <code>mysecureapp.mysecurens.svc.cluster.local</code>). Sinon Envoy rejette le certificat avec une erreur de SAN, même si la chaîne de confiance est parfaitement valide.</p>
<h3 id="2-distribuer-le-cacrt-avec-trust-manager">2. Distribuer le <code>ca.crt</code> avec trust-manager</h3>
<p><code>trust-manager</code> s'installe via Helm, dans le même namespace que <code>cert-manager</code> par défaut :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">$ helm upgrade --install trust-manager jetstack/trust-manager <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl">    --namespace cert-manager <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl">    --wait
</span></span></code></pre></div><p>⚠️ Remarque : trust-manager lit ses sources dans <strong>un seul namespace</strong>, défini par <code>--app.trust.namespace</code> (par défaut <code>cert-manager</code>, donc pas besoin de le spécifier). C'est pour ça qu'on a émis <code>internal-ca</code> directement dans <code>cert-manager</code> plutôt que de chercher à répliquer le secret depuis ailleurs.</p>
<p>Une fois trust-manager en place, on déclare un <code>Bundle</code> : il prend une source (notre secret <code>internal-ca-tls</code>) et <strong>synchronise</strong> un <code>ConfigMap</code> <strong>cible</strong> dans tous les namespaces qui matchent un selector :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">trust.cert-manager.io/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Bundle</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">sources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span>- <span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">ca.crt</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">target</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">configMap</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">ca.crt</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">namespaceSelector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="nt">trust-ca</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span></code></pre></div><p>Pour qu'un namespace récupère la CA, il suffit de le tagger :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl label namespace mysecurens trust-ca<span class="o">=</span><span class="nb">true</span>
</span></span></code></pre></div><p>Et c'est tout. Le <code>ConfigMap internal-ca</code> (clé <code>ca.crt</code>) apparaît automatiquement dans <code>mysecurens</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl -n mysecurens get cm internal-ca
</span></span><span class="line"><span class="ln">2</span><span class="cl">NAME          DATA   AGE
</span></span><span class="line"><span class="ln">3</span><span class="cl">internal-ca   <span class="m">1</span>      42m
</span></span></code></pre></div><p>Ce <code>ConfigMap</code> est ensuite référencé par la <code>BackendTLSPolicy</code> (cf. plus haut). Le jour où la CA tourne, <code>trust-manager</code> propage le nouveau <code>ca.crt</code> dans tous les namespaces consommateurs, sans rien à toucher.</p>
<h3 id="3-bonus--un-configmap-qui-ne-bouge-pas-et-cest-tant-mieux-">3. Bonus : un <code>ConfigMap</code> qui ne bouge pas, et c'est tant mieux 🎉</h3>
<p>Un effet de bord plutôt sympa de cette archi : tant que la <strong>CA</strong> ne tourne pas (soit dans 10 ans 😅), le <code>ConfigMap internal-ca</code> est strictement immuable. Les certificats <strong>feuilles</strong> de chaque appli, eux, peuvent tourner toutes les 90 jours sans que la <code>BackendTLSPolicy</code> ni le <code>ConfigMap</code> ne bougent — la chaîne de confiance reste valide tant que la racine ne change pas.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Avec la migration Gateway API, et malgré la perte du <code>SkipVerify</code>, on y gagne au final sur plusieurs plans :</p>
<ul>
<li>🔒 Du TLS de bout en bout, avec validation de la chaîne, pas du <code>verify: off</code> qui traîne dans une annotation</li>
<li>🧹 Une CA centralisée, des certificats feuilles auto-renouvelés, et un <code>ca.crt</code> qui se diffuse tout seul</li>
<li>📦 Tout en déclaratif Kubernetes, versionné, sans vieilles annotations 🙈 (on prend goût à ne plus gérer d'annotations tordues)</li>
</ul>
<p>Le ticket d'entrée est juste un poil plus élevé qu'une annotation <code>nginx</code>, mais le résultat est nettement plus solide. Et une fois la PKI interne en place, ajouter un nouveau backend HTTPS interne devient <em>très simple</em> : un <code>Certificate</code>, un label sur le namespace, une <code>BackendTLSPolicy</code>, et zou !</p>
<h2 id="ressources">Ressources</h2>
<ul>
<li><strong>Gateway API - BackendTLSPolicy</strong> : <a href="https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/">gateway-api.sigs.k8s.io/api-types/backendtlspolicy</a></li>
<li><strong>cert-manager</strong> : <a href="https://cert-manager.io/">cert-manager.io</a></li>
<li><strong>trust-manager</strong> : <a href="https://cert-manager.io/docs/trust/trust-manager/">cert-manager.io/docs/trust/trust-manager</a></li>
<li><strong>Envoy Gateway</strong> : <a href="https://gateway.envoyproxy.io/">gateway.envoyproxy.io</a></li>
</ul>]]></content:encoded></item><item><title>Mise-en-place &amp; Fnox : mon setup de gestion multi-projets</title><link>https://www.vrchr.fr/posts/2026/04/28/mise-en-place-fnox-setup-multi-projets/</link><pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2026/04/28/mise-en-place-fnox-setup-multi-projets/</guid><description>Comment j&amp;#39;utilise Mise au quotidien pour gérer mes versions d&amp;#39;outils, mes variables d&amp;#39;environnement et mes secrets sur plusieurs projets</description><content:encoded><![CDATA[<p>J'ai eu la chance de présenter <a href="https://mise.jdx.dev/">Mise</a> à <a href="https://www.devoxx.fr/">Devoxx France 2026</a> il y a quelques jours. J'utilise cet outil depuis plusieurs mois, et je trouvais intéressant de partager mon utilisation avec vous. Voici un retour d'expérience sur <strong>mon setup</strong>.</p>
<p>Si vous souhaitez une introduction plus complète vous pourrez bientôt voir la vidéo youtube, sinon les slides sont dispos <a href="https://presentations.verchere.fr/Mise_Devoxxfr_2026">ici</a>.</p>
<h2 id="le-problème--jongler-entre-contextes">Le problème : jongler entre contextes</h2>
<p>Je suis consultant Cloud Native Infra, et je gère au quotidien plus d'une <strong>dizaine de projets</strong> avec chacun leurs clusters Kubernetes en <strong>dev</strong> et en <strong>prod</strong>, hébergés sur des zones différentes. Chaque projet a ses propres versions d'outils, ses variables d'environnement, ses secrets, ses scripts...</p>
<p>Au quotidien, ça donne grosso modo ça :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">## Projet A - Dev - Helium ☕</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">$ java --version  <span class="c1"># openjdk 25.x</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">$ python --version  <span class="c1"># 3.14.x</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">$ kubectl version  <span class="c1"># v1.35.x</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">$ <span class="nb">export</span> <span class="nv">KUBECONFIG</span><span class="o">=</span>~/.kube/helium
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">$ <span class="nb">export</span> <span class="nv">ANSIBLE_INVENTORY</span><span class="o">=</span>inventories/helium/dev
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">## Projet B - Prod - Lithium ⚓</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">$ java --version  <span class="c1"># openjdk 17.x</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">$ python --version  <span class="c1"># 3.11.x</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">$ kubectl version  <span class="c1"># v1.33.x</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">$ <span class="nb">export</span> <span class="nv">KUBECONFIG</span><span class="o">=</span>~/.kube/lithium
</span></span><span class="line"><span class="ln">13</span><span class="cl">$ <span class="nb">export</span> <span class="nv">ANSIBLE_INVENTORY</span><span class="o">=</span>inventories/lithium/prod
</span></span></code></pre></div><p>Concrètement :</p>
<ul>
<li>🔄 Versions d'outils <strong>différentes</strong> selon les projets</li>
<li>🌍 Variables d'environnement <strong>spécifiques</strong> à chaque contexte</li>
<li>📜 Scripts <strong>custom</strong> à maintenir</li>
<li>⚠️ <strong>Oublier</strong> de switcher = bugs, erreurs, frustration (😱 le <code>terraform plan</code> sur le mauvais projet qui demande de redéployer un cluster, c'est arrivé ^^)</li>
</ul>
<p>Avant, je faisais tout cela avec un mix d'outils :</p>
<table>
	<thead>
			<tr>
					<th>Outil</th>
					<th>Ce qu'il fait</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>asdf</strong></td>
					<td>Gestion de versions d'outils</td>
			</tr>
			<tr>
					<td><strong>direnv</strong></td>
					<td>Variables d'env par répertoire</td>
			</tr>
			<tr>
					<td><strong>sdkman / venv / nvm</strong></td>
					<td>Version manager (1 langage)</td>
			</tr>
			<tr>
					<td><strong>Scripts shell</strong></td>
					<td>Tout le reste (notamment source de secrets)</td>
			</tr>
	</tbody>
</table>
<p>Ça marchait, mais c'était <em>perfectible</em>. Et puis j'ai (re)découvert <a href="https://mise.jdx.dev/">Mise</a>, un outil créé par <a href="https://github.com/jdx">@jdx</a> en 2023, écrit en <strong>Rust</strong> (donc <em>vachement rapide</em>), qui agrège dans un seul binaire <code>asdf</code> + <code>direnv</code> + un task runner, avec une activation automatique par répertoire, le tout configurable simplement avec des fichiers au format <code>TOML</code>.</p>
<p>Pour l'installation, c'est un simple <code>curl https://mise.run | sh</code> puis <code>eval &quot;$(mise activate zsh)&quot;</code> dans le shell. Je ne vais pas refaire la doc ici, vous trouverez tout sur <a href="https://mise.jdx.dev/">mise.jdx.dev</a>, je préfère vous montrer mon vrai setup.</p>
<h2 id="mon-setup--un-dizaine-de-projets--clusters-kubernetes">Mon setup : Un dizaine de projets &amp; clusters Kubernetes</h2>
<p>J'ai une arborescence du genre :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">projects-kubernetes-clusters/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">├── calcium/              # Projet A
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">│   ├── calcium-dev/
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│   └── calcium-prod/
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">├── lithium/              # Projet B
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│   ├── lithium-dev/
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│   └── lithium-prod/
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">├── helium/               # Projet C
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">│   ├── helium-dev/
</span></span><span class="line"><span class="ln">10</span><span class="cl">│   │   └── ansible/
</span></span><span class="line"><span class="ln">11</span><span class="cl">│   └── helium-prod/
</span></span><span class="line"><span class="ln">12</span><span class="cl">└── ...                   # 10+ projets identiques
</span></span></code></pre></div><p>Et la magie de Mise, c'est que la configuration est <strong>hiérarchique</strong> sur 3 niveaux :</p>
<ol>
<li><strong>Global</strong> (<code>~/.config/mise/config.toml</code>) : ce qui s'applique partout (proxy WireGuard par exemple)</li>
<li><strong>Projet parent</strong> (<code>calcium/mise.toml</code>) : variables OVH, endpoints, project name</li>
<li><strong>Environnement spécifique</strong> (<code>calcium/calcium-dev/mise.toml</code>) : nom du cluster, kubeconfig</li>
</ol>
<p><img alt="Architecture 3 niveaux Mise" class="zoomable" decoding="async" loading="lazy" src="/2026/04/2026-04-28-mise-architecture-3-niveaux.svg"></p>
<h3 id="niveau-global">Niveau global</h3>
<p>Au niveau de mon home, j'ai juste de quoi sourcer mon proxy WireGuard quand je suis chez un client :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># ~/.config/mise.toml</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">[</span><span class="nx">env</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c"># WireGuard Proxy</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">_</span><span class="p">.</span><span class="nx">source</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;~/.config/mise/proxy.sh&#34;</span><span class="p">]</span>
</span></span></code></pre></div><h3 id="niveau-projet-parent">Niveau projet parent</h3>
<p>Au niveau du projet parent, je définis les variables communes à tous les environnements (dev + prod), et j'active <code>fnox</code> pour les secrets :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># calcium/mise.toml</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">[</span><span class="nx">env</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nx">OS_REGION_NAME</span> <span class="p">=</span> <span class="s2">&#34;GRA7&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nx">OVH_ENDPOINT</span> <span class="p">=</span> <span class="s2">&#34;ovh-eu&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nx">OS_AUTH_URL</span> <span class="p">=</span> <span class="s2">&#34;https://auth.cloud.ovh.net/v3/&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nx">VAULT_ADDR</span> <span class="p">=</span> <span class="s2">&#34;https://vault.gravitek.io&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nx">VAULT_OVH_PROJECT</span> <span class="p">=</span> <span class="s2">&#34;calcium&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nx">TF_VAR_project_name</span> <span class="p">=</span> <span class="s2">&#34;{{env.VAULT_OVH_PROJECT}}&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c"># fnox pour les secrets</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nx">_</span><span class="p">.</span><span class="nx">fnox-env</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">tools</span> <span class="p">=</span> <span class="kc">true</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">[</span><span class="nx">plugins</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nx">fnox-env</span> <span class="p">=</span> <span class="s2">&#34;https://github.com/jdx/mise-env-fnox&#34;</span>
</span></span></code></pre></div><p>Les secrets sont gérés via <a href="https://fnox.jdx.dev/">fnox</a>, l'outil compagnon de Mise. Chez moi, j'utilise principalement <strong>HashiCorp Vault</strong> pour ce qui est partagé. La conf tient dans un <code>fnox.toml</code> au niveau du projet :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># calcium/fnox.toml</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">[</span><span class="nx">providers</span><span class="p">.</span><span class="nx">vault-ovh-project-api</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nx">type</span> <span class="p">=</span> <span class="s2">&#34;vault&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nx">address</span> <span class="p">=</span> <span class="s2">&#34;https://vault.gravitek.io&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nx">path</span> <span class="p">=</span> <span class="s2">&#34;secret/myorg/ovh/calcium&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">[</span><span class="nx">providers</span><span class="p">.</span><span class="nx">vault-ovh-project-openstack</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nx">type</span> <span class="p">=</span> <span class="s2">&#34;vault&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nx">address</span> <span class="p">=</span> <span class="s2">&#34;https://vault.gravitek.io&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nx">path</span> <span class="p">=</span> <span class="s2">&#34;secret/myorg/ovh/calcium/openstack&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">[</span><span class="nx">secrets</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nx">OVH_APPLICATION_KEY</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">provider</span> <span class="p">=</span> <span class="s2">&#34;vault-ovh-project-api&#34;</span><span class="p">,</span> <span class="nx">value</span> <span class="p">=</span> <span class="s2">&#34;API_CREDENTIALS/OVH_APPLICATION_KEY&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="nx">OVH_APPLICATION_SECRET</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">provider</span> <span class="p">=</span> <span class="s2">&#34;vault-ovh-project-api&#34;</span><span class="p">,</span> <span class="nx">value</span> <span class="p">=</span> <span class="s2">&#34;API_CREDENTIALS/OVH_APPLICATION_SECRET&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nx">OS_USERNAME</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">provider</span> <span class="p">=</span> <span class="s2">&#34;vault-ovh-project-openstack&#34;</span><span class="p">,</span> <span class="nx">value</span> <span class="p">=</span> <span class="s2">&#34;admin/OS_USERNAME&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nx">OS_PASSWORD</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">provider</span> <span class="p">=</span> <span class="s2">&#34;vault-ovh-project-openstack&#34;</span><span class="p">,</span> <span class="nx">value</span> <span class="p">=</span> <span class="s2">&#34;admin/OS_PASSWORD&#34;</span> <span class="p">}</span>
</span></span></code></pre></div><p>Et pour le <code>VAULT_TOKEN</code> lui-même, je le stocke dans le <strong>Keychain macOS</strong>, ce qui m'évite de l'avoir en clair quelque part :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">[</span><span class="nx">providers</span><span class="p">.</span><span class="nx">keychain</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">type</span> <span class="p">=</span> <span class="s2">&#34;keychain&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">service</span> <span class="p">=</span> <span class="s2">&#34;fnox&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">[</span><span class="nx">secrets</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">VAULT_TOKEN</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">provider</span> <span class="p">=</span> <span class="s2">&#34;keychain&#34;</span><span class="p">,</span> <span class="nx">value</span> <span class="p">=</span> <span class="s2">&#34;VAULT_TOKEN&#34;</span> <span class="p">}</span>
</span></span></code></pre></div><h3 id="niveau-environnement-spécifique">Niveau environnement spécifique</h3>
<p>Au niveau de l'environnement spécifique, j'ajoute le nom du cluster, l'URL, et surtout un <strong>hook</strong> qui auto-switch le contexte <code>kubectl</code> à l'entrée du répertoire :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># calcium/calcium-dev/mise.toml</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">[</span><span class="nx">env</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nx">CLUSTER_NAME</span> <span class="p">=</span> <span class="s2">&#34;calcium-dev&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nx">CLUSTER_URL</span> <span class="p">=</span> <span class="s2">&#34;https://xxx.k8s.ovh.net&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">[</span><span class="nx">hooks</span><span class="p">]</span>   <span class="c"># mode expérimental</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nx">enter</span> <span class="p">=</span> <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s2">if [ -n &#34;</span><span class="err">$</span><span class="nx">CLUSTER_NAME</span><span class="s2">&#34; ] &amp;&amp; kubectl config get-contexts &#34;</span><span class="err">$</span><span class="nx">CLUSTER_NAME</span><span class="s2">&#34; &amp;&gt;/dev/null; then
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s2">  kubectl config use-context &#34;</span><span class="err">$</span><span class="nx">CLUSTER_NAME</span><span class="s2">&#34;
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s2">fi
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span></code></pre></div><p>Plus de <code>kubectx</code> à la main, plus de risque de <code>terraform plan</code> sur le mauvais cluster 🙏.</p>
<h3 id="bonus--sous-répertoire-ansible-avec-venv-python">Bonus : sous-répertoire Ansible avec venv Python</h3>
<p>Pour mes répertoires Ansible, j'ajoute encore un niveau de configuration avec un venv Python dédié grâce à <code>uv</code>, et des tâches pour installer les dépendances :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># helium/helium-dev/ansible/mise.toml</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">[</span><span class="nx">tools</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nx">python</span> <span class="p">=</span> <span class="s2">&#34;3.14&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">[</span><span class="nx">env</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nx">_</span><span class="p">.</span><span class="nx">python</span><span class="p">.</span><span class="nx">venv</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">path</span> <span class="p">=</span> <span class="s2">&#34;.venv&#34;</span><span class="p">,</span> <span class="nx">create</span> <span class="p">=</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">python</span> <span class="p">=</span> <span class="s2">&#34;3.14&#34;</span><span class="p">,</span> <span class="nx">uv_create_args</span> <span class="p">=</span> <span class="p">[</span><span class="s1">&#39;--seed&#39;</span><span class="p">]</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">install</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Installer les dépendances et les rôles Ansible&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;uv pip install ansible ansible-lint python-gitlab&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">       <span class="s2">&#34;ansible-galaxy install -r roles/requirements.yml -p roles --force&#34;</span><span class="p">]</span>
</span></span></code></pre></div><h2 id="le-résultat-au-quotidien">Le résultat au quotidien</h2>
<p>Concrètement, en switchant de répertoire :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl">$ <span class="nb">cd</span> projects/calcium/calcium-dev/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">🔑 <span class="o">[</span>MISE<span class="o">]</span> Environnement CALCIUM détecté.
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">$ <span class="nb">echo</span> <span class="nv">$PROJECT_NAME</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">calcium
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">$ kubectl config current-context
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">calcium-dev
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">$ <span class="nb">echo</span> <span class="nv">$OVH_APPLICATION_SECRET</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">s3cr3t_p4ssw0rd      <span class="c1"># résolu depuis Vault automatiquement</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">$ <span class="nb">cd</span> ../../lithium/lithium-prod/
</span></span><span class="line"><span class="ln">11</span><span class="cl">🔑 <span class="o">[</span>MISE<span class="o">]</span> Environnement LITHIUM détecté.
</span></span><span class="line"><span class="ln">12</span><span class="cl">$ <span class="nb">echo</span> <span class="nv">$PROJECT_NAME</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">lithium
</span></span><span class="line"><span class="ln">14</span><span class="cl">$ kubectl config current-context
</span></span><span class="line"><span class="ln">15</span><span class="cl">lithium-prod
</span></span></code></pre></div><p>Je <code>cd</code> dans un répertoire, <strong>tout est automatiquement chargé</strong> : versions d'outils, variables d'env, secrets résolus depuis Vault ou le Keychain, contexte <code>kubectl</code> switché. Zéro friction, zéro oubli.</p>
<p><img alt="Context switch entre projets avec Mise" class="zoomable" decoding="async" loading="lazy" src="/2026/04/2026-04-28-mise-context-switch.svg"></p>
<p>Au final, Mise a remplacé chez moi : <code>asdf</code>, <code>direnv</code>, mes scripts shell custom, et même les virtualenvs python. C'est devenu <strong>le</strong> point d'entrée de mes projets.</p>
<h2 id="ressources">Ressources</h2>
<ul>
<li><strong>Documentation</strong> : <a href="https://mise.jdx.dev/">mise.jdx.dev</a> &amp; <a href="https://fnox.jdx.dev/">fnox.jdx.dev</a></li>
<li><strong>GitHub</strong> : <a href="https://github.com/jdx/mise">jdx/mise</a> &amp; <a href="https://github.com/jdx/fnox">jdx/fnox</a></li>
<li><strong>Slides Devoxx France 2026</strong> : <a href="https://presentations.verchere.fr/Mise_Devoxxfr_2026">presentations.verchere.fr/Mise_Devoxxfr_2026</a></li>
<li><strong>Dépôt de démo</strong> : <a href="https://github.com/gravitek-io/mise-demo">github.com/gravitek-io/mise-demo</a></li>
<li><strong>Blog Julien Wittouck</strong> : <a href="https://codeka.io/2025/12/19/adieu-direnv-bonjour-mise/">Adieu direnv, bonjour mise</a></li>
<li><strong>Blog Stéphane Robert</strong> : <a href="https://blog.stephane-robert.info/docs/outils/systeme/mise/">Mise</a></li>
</ul>]]></content:encoded></item><item><title>Créer et gérer plusieurs clusters Kubernetes Talos avec Docker</title><link>https://www.vrchr.fr/posts/2025/11/24/talos-docker-multi-clusters/</link><pubDate>Mon, 24 Nov 2025 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2025/11/24/talos-docker-multi-clusters/</guid><description>Créer et gérer plusieurs clusters Kubernetes Talos sur Docker avec communication inter-clusters</description><content:encoded><![CDATA[<p>J'ai récemment eu besoin de créer une architecture multi-clusters en local pour une démo ArgoCD. J'ai utilisé Talos Linux pour créer rapidement des clusters Kubernetes avec Docker sur mon laptop, mais j'ai rencontré quelques surprises lors de la mise en place d'un tel setup. Voici quelques pistes pour s'en sortir.</p>
<h2 id="préambule">Préambule</h2>
<p>Dans cet article on va causer réseau, avec des IPs pour joindre les divers nodes, qu'on devra définir. Voici un récapitulatif pour mon setup :</p>
<div style="display: table; margin: 0 auto;">
<table>
	<thead>
			<tr>
					<th>Cluster</th>
					<th>Node</th>
					<th>IP réseau dédié</th>
					<th>IP réseau partagé</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>cluster-1</td>
					<td>cluster-1-controlplane-1</td>
					<td>10.5.10.2</td>
					<td>172.30.0.11</td>
			</tr>
			<tr>
					<td>cluster-1</td>
					<td>cluster-1-worker-1</td>
					<td>10.5.10.3</td>
					<td>172.30.0.12</td>
			</tr>
			<tr>
					<td>cluster-2</td>
					<td>cluster-2-controlplane-1</td>
					<td>10.5.20.1</td>
					<td>172.30.0.21</td>
			</tr>
			<tr>
					<td>cluster-2</td>
					<td>cluster-2-worker-1</td>
					<td>10.5.20.2</td>
					<td>172.30.0.22</td>
			</tr>
	</tbody>
</table>
</div>
<p>Aussi, vous trouverez les scripts pour automatiser tout ce que je détaille dans l'article ici, utilisé pour ma conférence sur ArgoCD multi-clusters : <a href="https://github.com/gravitek-io/argocd-demo">dépôt git</a>.</p>
<p>On aura besoin de la cli <code>talosctl</code>. Pour information je suis en <code>v1.11.5</code> au moment de la publication de l'article :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ talosctl version  
</span></span><span class="line"><span class="ln">2</span><span class="cl">Client:
</span></span><span class="line"><span class="ln">3</span><span class="cl">        Tag:         v1.11.5
</span></span><span class="line"><span class="ln">4</span><span class="cl">        SHA:         undefined
</span></span><span class="line"><span class="ln">5</span><span class="cl">        Built:       2025-11-06T12:35:51Z
</span></span><span class="line"><span class="ln">6</span><span class="cl">        Go version:  go1.25.4
</span></span><span class="line"><span class="ln">7</span><span class="cl">        OS/Arch:     darwin/arm64
</span></span></code></pre></div><h2 id="création-de-plusieurs-clusters-talos">Création de plusieurs clusters Talos</h2>
<p>Commençons doucement : je veux créer deux clusters Talos locaux qui doivent communiquer entre eux pour tester ArgoCD en mode multi-clusters. Docker permet de créer 1 cluster Kind avec <a href="https://docs.docker.com/desktop/use-desktop/kubernetes/">Docker Desktop</a>, mais ici j'en ai besoin de plusieurs, autant m'amuser avec Talos.</p>
<p>La création de clusters est très simple si l'on suit la <a href="https://docs.siderolabs.com/talos/v1.11/platform-specific-installations/local-platforms/docker#multiple-clusters">documentation</a>. Chaque cluster devra alors avoir son propre nom et propre CIDR :</p>
<ul>
<li>Nom : <code>cluster-1</code></li>
<li>CIDR : <code>10.5.10.0/24</code></li>
<li>Version : <code>v1.33.4</code></li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl">$ talosctl cluster create --name cluster-1 --kubernetes-version v1.33.4 --cidr 10.5.10.0/24
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">validating CIDR and reserving IPs
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">generating PKI and tokens
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">creating state directory in <span class="s2">&#34;/path/to/talos/clusters/cluster-1&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">creating network cluster-1
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">creating controlplane nodes
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">creating worker nodes
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">waiting <span class="k">for</span> API
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">bootstrapping cluster
</span></span><span class="line"><span class="ln">10</span><span class="cl">waiting <span class="k">for</span> etcd to be healthy: OK
</span></span><span class="line"><span class="ln">11</span><span class="cl">waiting <span class="k">for</span> etcd members to be consistent across nodes: OK
</span></span><span class="line"><span class="ln">12</span><span class="cl">waiting <span class="k">for</span> etcd members to be control plane nodes: OK
</span></span><span class="line"><span class="ln">13</span><span class="cl">waiting <span class="k">for</span> apid to be ready: OK
</span></span><span class="line"><span class="ln">14</span><span class="cl">waiting <span class="k">for</span> <span class="o">[</span>...<span class="o">]</span>: OK
</span></span><span class="line"><span class="ln">15</span><span class="cl">waiting <span class="k">for</span> coredns to report ready: OK
</span></span><span class="line"><span class="ln">16</span><span class="cl">waiting <span class="k">for</span> all k8s nodes to report schedulable: OK
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">merging kubeconfig into <span class="s2">&#34;/path/to/kubeconfig&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">PROVISIONER           docker
</span></span><span class="line"><span class="ln">20</span><span class="cl">NAME                  cluster-1
</span></span><span class="line"><span class="ln">21</span><span class="cl">NETWORK NAME          cluster-1
</span></span><span class="line"><span class="ln">22</span><span class="cl">NETWORK CIDR          10.5.10.0/24
</span></span><span class="line"><span class="ln">23</span><span class="cl">NETWORK GATEWAY       10.5.10.1
</span></span><span class="line"><span class="ln">24</span><span class="cl">NETWORK MTU           <span class="m">1500</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">KUBERNETES ENDPOINT   https://127.0.0.1:62233
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl">NODES:
</span></span><span class="line"><span class="ln">28</span><span class="cl">
</span></span><span class="line"><span class="ln">29</span><span class="cl">NAME                        TYPE           IP          CPU    RAM      DISK
</span></span><span class="line"><span class="ln">30</span><span class="cl">/cluster-1-controlplane-1   controlplane   10.5.10.2   2.00   2.1 GB   -
</span></span><span class="line"><span class="ln">31</span><span class="cl">/cluster-1-worker-1         worker         10.5.10.3   2.00   2.1 GB   -
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ talosctl cluster create --name cluster-2 --kubernetes-version v1.33.4 --cidr 10.5.20.0/24
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">[</span>...<span class="o">]</span>
</span></span></code></pre></div><p>Après quelques minutes, j'ai mes deux clusters opérationnels :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl config get-contexts
</span></span><span class="line"><span class="ln">2</span><span class="cl">CURRENT   NAME              CLUSTER     AUTHINFO          NAMESPACE
</span></span><span class="line"><span class="ln">3</span><span class="cl">          admin@cluster-1   cluster-1   admin@cluster-1   default
</span></span><span class="line"><span class="ln">4</span><span class="cl">*         admin@cluster-2   cluster-2   admin@cluster-2   default
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl">$ NAME                       STATUS   ROLES           AGE     VERSION
</span></span><span class="line"><span class="ln">7</span><span class="cl">cluster-1-controlplane-1   Ready    control-plane   4m54s   v1.33.4
</span></span><span class="line"><span class="ln">8</span><span class="cl">cluster-1-worker-1         Ready    &lt;none&gt;          4m54s   v1.33.4
</span></span></code></pre></div><p>Parfait ! Maintenant, essayons de faire communiquer les deux clusters.</p>
<h2 id="visibilité-inter-clusters">Visibilité inter-clusters</h2>
<p>Lorsqu'on a créé les clusters, on a défini pour chacun un CIDR : <code>10.5.10.0/24</code> et <code>10.5.20.0/24</code>, ce qui correspond à 2 networks Docker :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl">$ docker network ls
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">NETWORK ID     NAME                    DRIVER    SCOPE
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">f2c1d59b61f2   cluster-1               bridge    <span class="nb">local</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">c31015d9e6d3   cluster-2               bridge    <span class="nb">local</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">$ docker network inspect cluster-1 -f <span class="s1">&#39;{{range .IPAM.Config}}{{.Subnet}}{{end}}&#39;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">10.5.10.0/24
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">$ docker network inspect cluster-2 -f <span class="s1">&#39;{{range .IPAM.Config}}{{.Subnet}}{{end}}&#39;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">10.5.20.0/24
</span></span></code></pre></div><p>Le problème est que chaque cluster a son propre réseau Docker isolé. Les conteneurs du cluster-1 ne peuvent pas joindre le réseau du cluster-2.</p>
<p><img alt="Réseaux Docker isolés" class="zoomable" decoding="async" loading="lazy" src="/2025/11/2025-11-24-talos-network-before.svg"></p>
<h3 id="création-dun-réseau-partagé">Création d'un réseau partagé</h3>
<p>Pour permettre la communication entre ces 2 clusters, il faut créer un réseau Docker commun et y connecter les nœuds de chaque cluster.</p>
<p>Créons d'abord un réseau partagé <code>talos-shared</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ docker network create --driver bridge --subnet<span class="o">=</span>172.30.0.0/24 talos-shared
</span></span><span class="line"><span class="ln">2</span><span class="cl">3e1f0e92331f7078ee72cd0cfa99a01bf35c9f012efc52e34b10fc3563e3d795
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">$ docker network ls
</span></span><span class="line"><span class="ln">5</span><span class="cl">f2c1d59b61f2   cluster-1               bridge    <span class="nb">local</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">c31015d9e6d3   cluster-2               bridge    <span class="nb">local</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">3e1f0e92331f   talos-shared            bridge    <span class="nb">local</span>
</span></span></code></pre></div><p>Maintenant, connectons les nœuds du cluster-1 à ce réseau :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ docker network connect --ip 172.30.0.11 talos-shared cluster-1-controlplane-1
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ docker network connect --ip 172.30.0.12 talos-shared cluster-1-worker-1
</span></span></code></pre></div><p>Et ceux du cluster-2 :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ docker network connect --ip 172.30.0.21 talos-shared cluster-2-controlplane-1
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ docker network connect --ip 172.30.0.22 talos-shared cluster-2-worker-1
</span></span></code></pre></div><p>Testons la connectivité réseau avec <code>kubectl debug</code> depuis le cluster 1 vers le 2 :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">kubectl --context admin@cluster-1 debug -n kube-system -it --image alpine node/cluster-1-worker-1 -- ping -c <span class="m">2</span> 172.30.0.21
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="m">64</span> bytes from 172.30.0.21: <span class="nv">seq</span><span class="o">=</span><span class="m">1</span> <span class="nv">ttl</span><span class="o">=</span><span class="m">64</span> <span class="nv">time</span><span class="o">=</span>0.529 ms
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">--- 172.30.0.21 ping statistics ---
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="m">2</span> packets transmitted, <span class="m">2</span> packets received, 0% packet loss
</span></span><span class="line"><span class="ln">6</span><span class="cl">round-trip min/avg/max <span class="o">=</span> 0.092/0.310/0.529 ms
</span></span></code></pre></div><p>Excellent ! Les nœuds se voient maintenant.</p>
<p><img alt="Réseau Docker partagé" class="zoomable" decoding="async" loading="lazy" src="/2025/11/2025-11-24-talos-network-after.svg"></p>
<p>Maintenant, tentons de nous connecter à l'API server du cluster-2 depuis le cluster-1. On va récupérer son IP :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl --context admin@cluster-2 config view --minify -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.clusters[0].cluster.server}&#39;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">https://127.0.0.1:62406
</span></span></code></pre></div><p>Bon, ça démarre mal... <code>127.0.0.1</code> est l'IP locale, pas sûr que ça fonctionne. En effet, lors de la création des clusters Talos + Docker, on passe par le host local pour accéder à l'API Server. Bon, on va quand même tenter avec l'IP du réseau partagé <code>172.30.0.21</code> !</p>
<p>Accédons à cette IP depuis un pod du cluster-1 sur le port par défaut de l'api-server, port <code>6443</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl">kubectl --context admin@cluster-1 debug -n kube-system -it --image alpine node/cluster-1-worker-1 -- /bin/sh
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">/ <span class="c1"># apk add curl</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">/ <span class="c1"># curl -k https://172.30.0.21:6443</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="o">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="s2">&#34;kind&#34;</span>: <span class="s2">&#34;Status&#34;</span>,
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="s2">&#34;apiVersion&#34;</span>: <span class="s2">&#34;v1&#34;</span>,
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="s2">&#34;metadata&#34;</span>: <span class="o">{}</span>,
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="s2">&#34;status&#34;</span>: <span class="s2">&#34;Failure&#34;</span>,
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="s2">&#34;message&#34;</span>: <span class="s2">&#34;Unauthorized&#34;</span>,
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="s2">&#34;reason&#34;</span>: <span class="s2">&#34;Unauthorized&#34;</span>,
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="s2">&#34;code&#34;</span>: <span class="m">401</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="o">}</span>/ <span class="c1">#</span>
</span></span></code></pre></div><p>Ok, on arrive à joindre l'API Server du cluster-2 depuis le cluster-1, on a une erreur 401 mais au moins ça répond !</p>
<p>Maintenant allons plus loin avec une commande <code>kubectl</code>. On édite le <code>kubeconfig</code> pour que le serveur soit bien <code>server: https://172.30.0.21:6443</code>, puis on exécute une commande depuis un conteneur dans le même sous-réseau pour valider l'accès à l'api-server :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ docker run --network talos-shared -v <span class="si">${</span><span class="nv">PWD</span><span class="si">}</span>:/tmp alpine/k8s:1.33.4 kubectl --kubeconfig<span class="o">=</span>/tmp/kubeconfig --context admin@cluster-2 get nodes
</span></span><span class="line"><span class="ln">2</span><span class="cl">pod:/# kubectl get nodes               
</span></span><span class="line"><span class="ln">3</span><span class="cl">NAME                       STATUS   ROLES           AGE   VERSION
</span></span><span class="line"><span class="ln">4</span><span class="cl">cluster-2-controlplane-1   Ready    control-plane   73m   v1.33.4
</span></span><span class="line"><span class="ln">5</span><span class="cl">cluster-2-worker-1         Ready    &lt;none&gt;          74m   v1.33.4
</span></span></code></pre></div><p>Ok, ça marche !</p>
<blockquote>
<p>Bon 🤔... quand j'avais testé pour ma démo avec ArgoCD, j'avais une erreur sur les Subject Alternative Names (certSANs), car Talos a généré un certificat seulement avec les 2 IPs initiales : celle en <code>10.5.20.2</code> et <code>127.0.0.1</code>. Si vous avez une erreur d'IP, alors on va recréer les clusters avec les bonnes SANs.</p>
</blockquote>
<h2 id="validité-du-certificat-de-lapi-server">Validité du certificat de l'API Server</h2>
<p>Le certificat de l'API server de Talos ne contient que les IPs du réseau dédié (<code>10.5.20.2</code>) et localhost. Il faut ajouter l'IP du réseau partagé (<code>172.30.0.21</code> par exemple pour le cluster-2) dans les Subject Alternative Names (SANs) du certificat.</p>
<p>On voit en effet par défaut qu'elle ne fait pas partie des certSANs :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ talosctl --context cluster-1 -n 127.0.0.1 get mc -o yaml <span class="p">|</span> yq <span class="s1">&#39;.spec | fromyaml | .cluster.apiServer.certSANs&#39;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 10.5.20.2
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 127.0.0.1
</span></span></code></pre></div><p>On va alors recréer les clusters avec un patch de configuration :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Suppression des clusters existants</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">$ talosctl cluster destroy --name cluster-1
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">$ talosctl cluster destroy --name cluster-2
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># Recréation avec patch des certSANs</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">$ talosctl cluster create <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl">    --name cluster-1 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl">    --kubernetes-version v1.33.4 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl">    --cidr 10.5.10.0/24 <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl">    --config-patch-control-plane <span class="s1">&#39;[{&#34;op&#34;: &#34;replace&#34;, &#34;path&#34;: &#34;/cluster/apiServer/certSANs&#34;, &#34;value&#34;: [&#34;172.30.0.11&#34;, &#34;10.5.10.2&#34;, &#34;127.0.0.1&#34;]}]&#39;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">$ talosctl cluster create <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl">    --name cluster-2 <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl">    --kubernetes-version v1.33.4 <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl">    --cidr 10.5.20.0/24 <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl">    --config-patch-control-plane <span class="s1">&#39;[{&#34;op&#34;: &#34;replace&#34;, &#34;path&#34;: &#34;/cluster/apiServer/certSANs&#34;, &#34;value&#34;: [&#34;172.30.0.21&#34;, &#34;10.5.20.2&#34;, &#34;127.0.0.1&#34;]}]&#39;</span>
</span></span></code></pre></div><p>On reconnecte ensuite les nœuds au réseau partagé :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ docker network connect --ip 172.30.0.11 talos-shared cluster-1-controlplane-1
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ docker network connect --ip 172.30.0.12 talos-shared cluster-1-worker-1
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ docker network connect --ip 172.30.0.21 talos-shared cluster-2-controlplane-1
</span></span><span class="line"><span class="ln">4</span><span class="cl">$ docker network connect --ip 172.30.0.22 talos-shared cluster-2-worker-1
</span></span></code></pre></div><p>Vérifions le certSANs :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ talosctl --context cluster-1 -n 127.0.0.1 get mc -o yaml <span class="p">|</span> yq <span class="s1">&#39;.spec | fromyaml | .cluster.apiServer.certSANs&#39;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 172.30.0.11
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 10.5.10.2
</span></span><span class="line"><span class="ln">4</span><span class="cl">- 127.0.0.1
</span></span></code></pre></div><p>On a maintenant un <code>certSANs</code> valide, des nodes accessibles depuis n'importe quel autre node partageant le réseau, impeccable !</p>
<h2 id="upgrade-kubernetes-inside-docker">Upgrade Kubernetes inside Docker</h2>
<p>Maintenant que j'ai mes clusters locaux qui peuvent discuter entre eux, je souhaite les mettre à jour. La <a href="https://docs.siderolabs.com/talos/v1.11/platform-specific-installations/local-platforms/docker#caveats">documentation</a> dit que <strong>non</strong>, mais je suis joueur !</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ talosctl --context cluster-1 -n 127.0.0.1 upgrade-k8s --to 1.34.0
</span></span><span class="line"><span class="ln">2</span><span class="cl">error detecting the lowest Kubernetes version Get <span class="s2">&#34;https://10.5.10.2:6443/api/v1/namespaces/kube-system/pods&#34;</span>: dial tcp 10.5.10.2:6443: i/o timeout
</span></span></code></pre></div><p>Ah ! En effet, ça ne fonctionne pas... Le problème est que <code>talosctl</code> essaie de communiquer avec le control plane via <code>10.5.10.2:6443</code>, mais depuis l'hôte Docker, on doit joindre le control plane du conteneur via localhost (ce qui est défini dans notre <code>KUBECONFIG</code>).</p>
<p>Si on regarde la configuration de la machine, l'endpoint du control plane pointe vers <code>https://10.5.10.2:6443</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ talosctl -n 127.0.0.1 get mc -o yaml <span class="p">|</span> yq <span class="s1">&#39;.spec | fromyaml | .cluster.controlPlane.endpoint&#39;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">https://10.5.10.2:6443
</span></span></code></pre></div><h3 id="patcher-temporairement-lendpoint">Patcher temporairement l'endpoint</h3>
<p>Pour faire l'upgrade, on va temporairement changer l'endpoint du control plane pour qu'il pointe vers l'IP du réseau local, faire l'upgrade, puis remettre la configuration d'origine. Voici comment faire pour le cluster-1 :</p>
<ol>
<li>
<p>Récupérer l'URL du serveur depuis le kubeconfig</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ <span class="nv">SERVER_URL</span><span class="o">=</span><span class="k">$(</span>kubectl config view --context admin@cluster-1 --minify <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl">    -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.clusters[0].cluster.server}&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ <span class="nb">echo</span> <span class="nv">$SERVER_URL</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">https://127.0.0.1:51189
</span></span></code></pre></div></li>
<li>
<p>Sauvegarder l'ancien endpoint sur le réseau dédié au cluster (<code>10.5.10.2</code>)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ <span class="nv">OLD_URL</span><span class="o">=</span><span class="k">$(</span>talosctl --context cluster-1 -n 127.0.0.1 get mc -o yaml <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl">    yq <span class="s1">&#39;.spec | fromyaml | .cluster.controlPlane.endpoint&#39;</span> <span class="p">|</span> grep <span class="s1">&#39;^https&#39;</span> <span class="p">|</span> head -n1<span class="k">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ <span class="nb">echo</span> <span class="nv">$OLD_URL</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">https://10.5.10.2:6443
</span></span></code></pre></div></li>
<li>
<p>Patcher l'endpoint pour utiliser l'IP du réseau localhost avec <code>$SERVER_URL</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ talosctl --context cluster-1 -n 127.0.0.1 patch mc <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl">    --patch <span class="s1">&#39;[{&#34;op&#34;: &#34;replace&#34;, &#34;path&#34;: &#34;/cluster/controlPlane/endpoint&#34;, &#34;value&#34;: &#34;https://127.0.0.1:51189&#34;}]&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">patched MachineConfigs.config.talos.dev/v1alpha1 at the node 127.0.0.1
</span></span><span class="line"><span class="ln">4</span><span class="cl">Applied configuration without a reboot
</span></span></code></pre></div></li>
<li>
<p>Lancer l'upgrade Kubernetes depuis notre laptop !</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl">$ talosctl --context cluster-1 -n 127.0.0.1 upgrade-k8s --to 1.34.0
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">automatically detected the lowest Kubernetes version 1.33.4
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">discovered controlplane nodes <span class="o">[</span><span class="s2">&#34;10.5.20.2&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">discovered worker nodes <span class="o">[</span><span class="s2">&#34;10.5.20.3&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">&gt; <span class="s2">&#34;10.5.20.2&#34;</span>: Talos version 1.11.5 is compatible with Kubernetes version 1.34.0
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">&gt; <span class="s2">&#34;10.5.20.3&#34;</span>: Talos version 1.11.5 is compatible with Kubernetes version 1.34.0
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">checking <span class="k">for</span> removed Kubernetes component flags
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">checking <span class="k">for</span> removed Kubernetes API resource versions
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">&gt; <span class="s2">&#34;10.5.20.2&#34;</span>: pre-pulling registry.k8s.io/kube-apiserver:v1.34.0
</span></span><span class="line"><span class="ln">10</span><span class="cl">&gt; <span class="s2">&#34;10.5.20.2&#34;</span>: pre-pulling registry.k8s.io/kube-controller-manager:v1.34.0
</span></span><span class="line"><span class="ln">11</span><span class="cl">&gt; <span class="s2">&#34;10.5.20.2&#34;</span>: pre-pulling registry.k8s.io/kube-scheduler:v1.34.0
</span></span><span class="line"><span class="ln">12</span><span class="cl">&gt; <span class="s2">&#34;10.5.20.2&#34;</span>: pre-pulling ghcr.io/siderolabs/kubelet:v1.34.0
</span></span><span class="line"><span class="ln">13</span><span class="cl">&gt; <span class="s2">&#34;10.5.20.3&#34;</span>: pre-pulling ghcr.io/siderolabs/kubelet:v1.34.0
</span></span><span class="line"><span class="ln">14</span><span class="cl">updating <span class="s2">&#34;kube-apiserver&#34;</span> to version <span class="s2">&#34;1.34.0&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">&gt; <span class="s2">&#34;10.5.20.2&#34;</span>: starting update
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="o">[</span>...<span class="o">]</span>
</span></span></code></pre></div></li>
<li>
<p>Une fois terminé, remettre l'endpoint d'origine (<code>$OLD_URL</code>)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ talosctl --context cluster-1 -n 127.0.0.1 patch mc <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl">    --patch <span class="s1">&#39;[{&#34;op&#34;: &#34;replace&#34;, &#34;path&#34;: &#34;/cluster/controlPlane/endpoint&#34;, &#34;value&#34;: &#34;https://10.5.10.2:6443&#34;}]&#39;</span>
</span></span></code></pre></div></li>
<li>
<p>Redémarrer les conteneurs pour appliquer la configuration</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ docker container restart cluster-1-controlplane-1 cluster-1-worker-1
</span></span></code></pre></div></li>
</ol>
<p>Vérifions que l'upgrade a bien fonctionné :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl --context admin@cluster-1 get nodes
</span></span><span class="line"><span class="ln">2</span><span class="cl">NAME                       STATUS     ROLES           AGE   VERSION
</span></span><span class="line"><span class="ln">3</span><span class="cl">cluster-1-controlplane-1   Ready      control-plane   18m   v1.34.0
</span></span><span class="line"><span class="ln">4</span><span class="cl">cluster-1-worker-1         Ready      &lt;none&gt;          18m   v1.34.0
</span></span></code></pre></div><p>Parfait ! L'upgrade Kubernetes a réussi 🎉, même si la doc officielle nous dit que c'est impossible 😜. Le redémarrage des conteneurs permet de s'assurer que la configuration d'origine (avec l'endpoint localhost) est bien appliquée.</p>
<p><img alt="Récapitulatif du workflow d'upgrade" class="zoomable" decoding="async" loading="lazy" src="/2025/11/2025-11-24-talos-upgrade-k8s.svg"></p>
<h2 id="conclusion">Conclusion</h2>
<p>Créer plusieurs clusters Talos locaux avec Docker est simple, mais permettre leur communication nécessite quelques ajustements :</p>
<ol>
<li><strong>Créer un réseau Docker partagé</strong> pour que les nœuds puissent communiquer</li>
<li><strong>Patcher les certSANs</strong> lors de la création pour inclure l'IP du réseau partagé dans le certificat TLS</li>
<li><strong>Patcher temporairement le controlPlane endpoint</strong> lors des upgrades Kubernetes pour utiliser l'IP du host</li>
</ol>
<p>Avec cette approche, on peut s'amuser à gérer tout plein de clusters localement avec Docker, faut-il encore avoir une machine qui supporte autant de nodes 🤯 !</p>
<h3 id="ressources">Ressources</h3>
<p>Quelques ressources utiles pour aller plus loin :</p>
<ul>
<li><a href="https://www.talos.dev/">Documentation officielle Talos</a></li>
<li><a href="https://www.talos.dev/latest/talos-guides/install/local-platforms/docker/">Talos Docker Mode</a></li>
<li><a href="https://www.talos.dev/latest/talos-guides/configuration/patching/">Machine Config Patches</a></li>
<li><a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/declarative-setup/#clusters">ArgoCD Multi-Clusters</a></li>
<li><a href="https://github.com/gravitek-io/argocd-demo/tree/main/scripts">Scripts utilisés pour la démo ArgoCD</a></li>
</ul>
<h3 id="genai">GenAI</h3>
<blockquote>
<p>Je me suis aidé de <a href="https://www.claude.com/product/claude-code">Claude Code</a> pour rédiger l'article, même si au final j'ai ré-écrit plus de 80% de l'article après beaucoup de discussions pour valider les commandes et corriger ce que je voulais vraiment présenter. Il m'a été très utile cependant pour la création des images SVG.</p>
</blockquote>]]></content:encoded></item><item><title>Xiaomi Mijia Zigbee to Bluetooth</title><link>https://www.vrchr.fr/posts/2025/11/23/xiaomi-mijia-zigbee-to-bt/</link><pubDate>Sun, 23 Nov 2025 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2025/11/23/xiaomi-mijia-zigbee-to-bt/</guid><description>Changer le protocole de communication de vos Xiaomi Mijia de Zigbee à BT Home</description><content:encoded><![CDATA[<p>Il y a quelques jours, <a href="https://blog.zwindler.fr/2025/11/15/flasher-vos-thermom%C3%A8tres-xiaomi-mijia-pour-prolonger-la-batterie-dun-an/">Zwindler</a> a sorti un article pour flasher des thermomètres Xiaomi Mijia. J'ai les mêmes, dont certains flashés pour échanger via le protocole Zigbee, mais toujours trop consommateur à mon goût.</p>
<p>J'ai cherché alors comment revenir sur du Bluetooth LE (<a href="https://fr.wikipedia.org/wiki/Bluetooth_%C3%A0_basse_consommation">Low Energy</a>). Voici un court article sur le sujet.</p>
<p>Tout d'abord, j'utilise <a href="https://www.home-assistant.io/">Home Assistant</a> depuis quelques années, mais je n'ai pas grand chose dessus. Je l'utilise principalement pour centraliser le suivi de température de mon logement. Pour cela, j'ai opté pour des thermomètres Xiaomi Mijia <code>LYWSD03MMC</code>, avec un très bon rapport qualité/prix, facile à utiliser &amp; flasher pour l'intégrer dans Home Assistant.</p>
<p>J'avais d'abord paramétré les thermomètres avec le protocole <a href="https://www.home-assistant.io/integrations/xiaomi_ble/">Xiaomi BLE</a>, puis basculé certains en Zigbee grâce notamment au firmware <a href="https://github.com/devbis/z03mmc">z03mmc</a>. Vous trouverez suffisamment de tutos sur Internet si vous souhaitez basculer en zigbee.</p>
<p>Cependant, dans l'article de Denis il parle de <a href="https://bthome.io/">BTHome v2</a>, option que je n'avais pas avant (seulement Xiaomi BLE). J'ai donc décidé de tout migré sur ce protocole qui me semble plus standard et mieux supporté.</p>
<p><img alt="BTHome" class="zoomable" decoding="async" loading="lazy" src="/2025/11/2025-11-23-bthome-overview.webp"></p>
<p>Ci-après vous trouverez alors la procédure pour revenir de Zigbee à Bluetooth BLE, moins courrant que l'inverse 🙂</p>
<h2 id="home-assistant-zha--ota">Home Assistant ZHA &amp; OTA</h2>
<p>Mon setup utilise <a href="https://www.home-assistant.io/integrations/zha/">Zigbee Home Automation</a>, pour contrôler mes équipements Zigbee, et donc certains de mes thermomètres jusqu'à présent. J'ai pu voir d'autres setups utilisant du MQTT avec Zigbee2MQTT &amp; Mosquito, ce n'est pas mon cas.</p>
<h3 id="zha-toolkit">ZHA toolkit</h3>
<p>Pour pouvoir flasher à nouveau les devices, il va falloir alors que je passe par de l'OTA (over-the-air). ZHA le supporte nativement. Cependant, pour accélérer le processus, on peut installer <a href="https://github.com/mdeweerd/zha-toolkit">ZHA Toolkit</a> :</p>
<ol>
<li>Installer <a href="https://hacs.xyz/">Home Assistant Community Store</a></li>
<li>Installer ZHA Toolkit</li>
</ol>
<p>Je ne sais pas si cela est vraiment utile, mais &quot;chezmoiçamarche&quot;.</p>
<p><img alt="ZHA Toolkit" class="zoomable" decoding="async" loading="lazy" src="/2025/11/2025-11-23-zha-toolkit.webp"></p>
<h3 id="settings-pour-ota">Settings pour OTA</h3>
<p>On va maintenant définir dans ZHA où le thermomètre devra aller récupérer le firmware. Une longue liste est disponible sur le dépôt GitHub de <a href="https://github.com/pvvx/ATC_MiThermometer/tree/master/zigbee_ota">pvvx</a>.</p>
<p>On édite le fichier <code>/homeassitant/configuration.yaml</code> (peut dépendre de votre installation) pour :</p>
<ol>
<li>Activer ZHA Toolkit</li>
<li>Spécifier l'URL de la liste des firmwares à récupérer (pour moi : <code>index_v55.json</code> du dépôt GIT)</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">zha_toolkit</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nt">zha</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">zigpy_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="nt">ota</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">      </span><span class="nt">z2m_remote_index</span><span class="p">:</span><span class="w"> </span><span class="l">https://raw.githubusercontent.com/pvvx/ATC_MiThermometer/ef4b5a713d16acfe3f3183d72b7706c9fc697c86/zigbee_ota/index_v55.json</span><span class="w">
</span></span></span></code></pre></div><p>Puis on redémarre HomeAssistant pour la prise en compte.</p>
<h3 id="flash">Flash</h3>
<p>Tout est prêt, reste maintenant à flasher ! 2 options possibles, soit vous forcez l'OTA avec la notification <code>zha_toolkit</code>, soit vous attendez...</p>
<p>Pour &quot;trigger&quot; un OTA, on va dans les &quot;Outils de développement&quot;, puis &quot;Actions&quot;, on sélectionne <code>zha_toolkit.ota_notify</code> et on choisit le thermomètre que l'on souhaite mettre à jour :</p>
<p><img alt="ZHA Toolkit Trigger" class="zoomable" decoding="async" loading="lazy" src="/2025/11/2025-11-23-zha-toolkit-trigger.webp"></p>
<p>Après quelques instants, HomeAssitant propose une mise à jour !</p>
<p><img alt="ZHA OTA Notification" class="zoomable" decoding="async" loading="lazy" src="/2025/11/2025-11-23-zha-ota-notif.webp"></p>
<blockquote>
<p>Note: on voit ici un &quot;Entity not found&quot;, c'est parce que le screenshot a été pris après que j'ai supprimé le device, ne pas en tenir compte.</p>
</blockquote>
<p>On lance l'upgrade, et on attend que ça se termine.</p>
<p><img alt="ZHA OTA Progress" class="zoomable" decoding="async" loading="lazy" src="/2025/11/2025-11-23-zha-ota-progress.webp"></p>
<h2 id="configuration-bluetooth">Configuration Bluetooth</h2>
<p>A partir de ce moment, on peut suivre la procédure de Denis sur son blog, en utilisant le flasher en ligne <a href="https://pvvx.github.io/ATC_MiThermometer/TelinkMiFlasher.html">TelinkMiFlasher</a>.</p>
<h2 id="intégration-home-assistant">Intégration Home Assistant</h2>
<p>Après la reconfiguration, le device sera détecté par HomeAssistant, reste plus qu'à le rajouter !</p>
<p><img alt="Detection BTHome" class="zoomable" decoding="async" loading="lazy" src="/2025/11/2025-11-23-bthome-detect.webp"></p>
<h3 id="suppression--historique">Suppression &amp; Historique</h3>
<p>Attention (c'est bien de lire jusqu'ici), si vous souhaitez conserver l'historique des températures, humidité &amp; co, pensez à bien noter les identités de l'ancien device zigbee avant de le supprimer, et modifiez celles du nouveau device, pour mon cas :</p>
<ul>
<li><code>temperature_humidity_sensor_1bbb_battery</code></li>
<li><code>temperature_humidity_sensor_1bbb_humidity</code></li>
<li><code>temperature_humidity_sensor_1bbb_temperature</code></li>
<li><code>temperature_humidity_sensor_1bbb_voltage</code></li>
</ul>
<h2 id="sources">Sources</h2>
<p>Comme je disais en début d'article, beaucoup de ressources pour basculer de bluetooth à zigbee, mais peu pour l'inverse. Ce qui m'a permis d'y arriver est surtout ce commentaire GitHub :</p>
<ul>
<li><a href="https://github.com/pvvx/ZigbeeTLc/issues/151#issuecomment-2645922817">https://github.com/pvvx/ZigbeeTLc/issues/151#issuecomment-2645922817</a></li>
</ul>
<p>Maintenant vous pouvez utiliser correctement vos thermomètres en Bluetooth BLE !</p>]]></content:encoded></item><item><title>Bye bye VMware</title><link>https://www.vrchr.fr/posts/2025/11/22/new-theme/</link><pubDate>Sat, 22 Nov 2025 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2025/11/22/new-theme/</guid><description>Bye bye VMware ! Nouveau theme pour le blog</description><content:encoded><![CDATA[<p>L'année 2025 est riche en changements ! Nouveau job chez <a href="https://www.gravitek.io">Gravitek</a> entre autres, et niveau tech si vous n'avez pas suivi, l'arrêt de la mise à disposition des images &amp; charts helm bitnami sur dockerhub. Du coup, pas mal de migrations à prévoir !</p>
<p>Et tant qu'à migrer, pourquoi ne pas changer un peu de theme du blog ? Ah, vous pensiez à autre chose ? Désolé de vous décevoir 😄</p>
<p>En effet, le thème utilisé jusqu'à présent était <a href="https://github.com/chipzoller/hugo-clarity">hugo-clarity</a>, basé sur le design system <a href="https://clarity.design/">Clarity</a> de VMware.  Il était alors tant d'aller voir ailleurs... Après quelques recherches, j'ai choisi simplement un des thèmes les plus connus Hugo : <a href="https://adityatelange.github.io/hugo-PaperMod/">PaperMod</a>. Je recherchais quelque chose de simple et épuré, bien supporté. Et ce thème correspond à tous mes critères, donc feu !</p>
<h2 id="changement-du-thème">Changement du thème</h2>
<p>Pour changer de thème, quelques adaptations à faire, mais la doc est assez simple à suivre :</p>
<ol>
<li><a href="https://github.com/adityatelange/hugo-PaperMod/wiki/Installation">Installation</a> du thème à coup de <code>git submodule</code></li>
<li>Modification des pages pour mieux intégrer les images de cover (<code>thumbnail</code> vers <code>cover</code>)</li>
<li>Gestion de la table des matières</li>
<li>Gestion d'asciinema</li>
</ol>
<p>Exemple de changements :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="gh">diff --git a/content/posts/2021/2021-09-09-marp-gitpod.md b/content/posts/2021/2021-09-09-marp-gitpod.md
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="gh">index a74584f..22cd983 100644
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="gd">--- a/content/posts/2021/2021-09-09-marp-gitpod.md
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="gi">+++ b/content/posts/2021/2021-09-09-marp-gitpod.md
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="gu">@@ -1,14 +1,16 @@
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"> ---
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"> title: &#34;Créer des slides depuis n&#39;importe où avec Marp, GitLab Pages et Gitpod&#34;
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"> date: 2021-09-09T12:00:00Z
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="gd">-thumbnail: /2021/09/2021-09-09-marp.png
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="gi">+cover:
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="gi">+  image: /2021/09/2021-09-09-marp.png
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="gi">+  alt: &#34;Logo Marp - outil de création de présentations en Markdown&#34;
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="gi">+  relative: false
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"> categories:
</span></span><span class="line"><span class="ln">15</span><span class="cl">   - Divers
</span></span><span class="line"><span class="ln">16</span><span class="cl"> tags:
</span></span><span class="line"><span class="ln">17</span><span class="cl">   - GitLab
</span></span><span class="line"><span class="ln">18</span><span class="cl">   - Markdown
</span></span><span class="line"><span class="ln">19</span><span class="cl"> description: Créer des slides depuis n&#39;importe où avec Marp, GitLab Pages et Gitpod.
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="gd">-toc: true
</span></span></span></code></pre></div><h2 id="améliorations">Améliorations</h2>
<p>Basculer de thème m'a permis de corriger quelques trucs qui ne fonctionnaient pas avant, ou ne me convenaient pas :</p>
<ul>
<li>L'affichage des tags / catégories &amp; co dont je me fiche, je voulais quelque chose de minimaliste</li>
<li>Mise en place des permalinks plus propres (déso pour les flux RSS, j'ai l'impression que ça a regénéré de nouveaux articles)</li>
<li>Nettoyage des catégories et tags</li>
<li>La recherche sur les articles <a href="/search/">ici</a></li>
</ul>
<figure><img class="zoomable" loading="lazy" decoding="async" src="/2025/11/2025-11-22-recherche.webp"
         alt="Page de recherche" width="600"/> 
</figure>

<h2 id="le-design-merci-claude">Le design, merci Claude</h2>
<p>Tant qu'a changer, autant faire les choses jusqu'au bout, j'ai demandé à <a href="https://claude.ai/">Claude</a> de me faire un css sur mesure, aux couleurs de ma société <a href="https://www.gravitek.io/">Gravitek</a>.</p>
<p>Après quelques heures de discussions, d'essais, j'arrive à un résultat pas trop mal :</p>
<ul>
<li>Couleurs qui me plaisent, avec theme ligth/dark</li>
<li>Gestion des images / code correct</li>
<li>Gestion du format mobile</li>
</ul>
<p>Au départ c'était mal barré !</p>

<div style="display: flex; gap: 10px; justify-content: center; align-items: center; flex-wrap: wrap;">
  
    
    
      
    
  <img class="zoomable" src="/2025/11/2025-11-22-claude-1.webp" alt="Instructions Claude" loading="lazy" decoding="async" style="max-width: 48%; height: auto;">
  
  
    
    
      
    
  <img class="zoomable" src="/2025/11/2025-11-22-claude-2.webp" alt="Résultat Claude" loading="lazy" decoding="async" style="max-width: 48%; height: auto;">
  
</div>

<p>Bon, Claude est gentil par contre il est très verbeux, la CSS custom fait pas moins de <a href="https://gitlab.com/rverchere/tech-notes/-/blob/main/assets/css/extended/custom.css?ref_type=heads">1000 lignes</a> ! Si vous avez lu jusque là, que vous êtes experts CSS et que vous voulez améliorer ça, n'hésitez pas à faire une <a href="https://gitlab.com/rverchere/tech-notes/-/merge_requests">Merge Request</a> 😇</p>
<p>Egalement, si vous trouvez des bugs d'affichage n'hésitez pas à m'en faire part !</p>
<h2 id="la-suite">La suite</h2>
<p>Est-ce qu'avec ce nouveau site je vais écrire de nouveaux articles ? Probable ! On verra ces prochaines semaines ! 😉</p>]]></content:encoded></item><item><title>Récupérer les logs d'audit MKS avec Vector</title><link>https://www.vrchr.fr/posts/2025/08/29/vector-ldp-websocket/</link><pubDate>Fri, 29 Aug 2025 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2025/08/29/vector-ldp-websocket/</guid><description>Comment récupérer les logs d&amp;#39;audit de clusters MKS via la Logs Data Plaform OVHCloud avec Vector</description><content:encoded><![CDATA[<p>J'administre au quotidien des clusters managés OVHCloud. Parmi mes tâches, je dois gérer la stack d'observabilité, et notamment avoir les logs des applis et autres composants du cluster.</p>
<p>Autant, récupérer les logs des applis du cluster c'est simple à coup de Promtail/Alloy/Loki, ELK ou autre stack, mais pour récupérer les logs d'audit du control-plane ce n'était pas évident... Jusqu'à maintenant 😉</p>
<h2 id="ovhcloud-managed-kubernetes-service--logs-data-platform">OVHCloud Managed Kubernetes Service &amp; Logs Data Platform</h2>
<p>La <a href="https://www.ovhcloud.com/fr/identity-security-operations/logs-data-platform/">Logs Data Platform</a> (aka &quot;LDP&quot;) permet de centraliser les logs. Je ne ferai pas une présentation de cette plateforme, car j'utilise ma propre stack, et j'ai des besoins d'archivage/export de logs sur d'autres environnements, pour de l'archivage, SOC, etc.</p>
<p>Cette Logs Data Platform est notamment utilisée pour permettre l'envoi des logs d'audit du control-plane des clusters MKS, sur lesquels nous n'avons pas la main.</p>
<p>En effet, depuis l'interface du manager OVHCloud, on peut transférer les logs :</p>
<p><img alt="Logs MKS" class="zoomable" decoding="async" loading="lazy" src="/2025/08/2025-08-29-manager-ldp.webp"></p>
<p>Pour paramétrer l'envoi de ces logs, je vous renvoie sur la <a href="https://help.ovhcloud.com/csm/fr-public-cloud-kubernetes-forwarding-audit-logs?id=kb_article_view&amp;sysparm_article=KB0062284">doc officielle</a>.</p>
<h2 id="vector--websocket">Vector &amp; websocket</h2>
<p>OK, jusque là j'ai mes logs sur la &quot;LDP&quot;, mais je veux pouvoir les récupérer chez moi (ou pouvoir les envoyer ailleurs) !</p>
<p>A la suite d'articles comme <a href="https://blog.ovhcloud.com/enhancing-kubernetes-security-detecting-threats-in-ovhcloud-managed-kubernetes-cluster-mks-audit-logs-with-falco/">Falco</a>, j'ai découvert qu'il était possible de récupérer les logs via <code>websocket</code> ! Et si, par chance, il n'existerait pas un outil pour ça ?</p>
<h3 id="vector">Vector</h3>
<p>Vous l'aurez deviné, <a href="https://vector.dev/">Vector</a> est un outil qui permet de récupérer, transformer, envoyer des logs, métriques et traces, un très bon outil d'observabilité !</p>
<p>Parmi ses qualités, on notera :</p>
<ul>
<li>sa rapidité de traitement vs sa faible empreinte cpu/mémoire (oui, c'est écrit en rust)</li>
<li>son écosystème de plugins riches</li>
<li>sa facilité de déploiement.</li>
</ul>
<p><img alt="Architecture Vectordev" class="zoomable" decoding="async" loading="lazy" src="/2025/08/2025-08-29-vectordev-archi.webp"></p>
<p>Et, &quot;comme par hasard&quot;, la version <a href="https://vector.dev/releases/0.49.0/">v0.49.0</a> vient de sortir avec une fonctionnalité qui m'intéresse :</p>
<blockquote>
<p>&quot;A websocket source was introduced. A WebSocket source in Vector enables ingestion of real-time data from services that expose WebSocket APIs.&quot;</p>
</blockquote>
<p>Bon, ben y a plus qu'à !!</p>
<h3 id="paramétrage--test-local">Paramétrage &amp; test local</h3>
<p>Avant de déployer ça sur mes clusters, on va tester ça localement, pour éviter trop de <code>git push</code>, <code>rollout</code> et <code>CrashLoopBackoff</code> 😅. Etant sous MacOS, l'install se fait facilement avec <code>brew</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ brew tap vectordotdev/brew &amp;&amp; brew install vector
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ vector --version
</span></span><span class="line"><span class="ln">3</span><span class="cl">vector 0.49.0 (aarch64-apple-darwin dc7e792 2025-08-12 13:47:08.632326804)
</span></span></code></pre></div><p>Ensuite, il faut :</p>
<ol>
<li>
<p>Récupérer l'URL websocket</p>
<ul>
<li>Depuis l'interface OVH, allez dans l'onglet &quot;Flux de Données&quot;, &quot;Suivre en direct&quot;</li>
<li>Puis cliquer sur &quot;Actions&quot;, &quot;Copier l'adresse WebSocket&quot;</li>
<li>L'adresse sera de type <code>wss://gra1.logs.ovh.com/tail/?tk=abcdefghijklmnopqrstuvwxyz</code></li>
</ul>
</li>
<li>
<p>Paramétrer Vector, en éditant un fichier <code>vector.yaml</code></p>
<ul>
<li>Définir un objet <code>source</code> de type <code>websocket</code></li>
<li>Définir les <code>transforms</code> souhaitées et les <code>sinks</code> (destinations)</li>
</ul>
</li>
</ol>
<p>Pour l'exemple, on va simplement renvoyer sur la console :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># vector-1.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">sources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">ovh_ldp_source</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">websocket</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">uri</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;wss://gra1.logs.ovh.com/tail/?tk=abcdefghijklmnopqrstuvwxyz&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">tls</span><span class="p">:</span><span class="w"> </span><span class="c"># le websocket OVHCloud est en TLS, sinon vous aurez une erreur de connection ^^</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nt">sinks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">stdout_sink</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">console</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span>- <span class="l">ovh_ldp_source</span><span class="w">
</span></span></span></code></pre></div><p>On lance !</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ vector -c vector-1.yaml
</span></span><span class="line"><span class="ln">2</span><span class="cl">2025-08-29T09:05:09.371791Z  INFO vector::app: Log level is enabled. <span class="nv">level</span><span class="o">=</span><span class="s2">&#34;info&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">2025-08-29T09:05:09.373257Z  INFO vector::app: Loading configs. <span class="nv">paths</span><span class="o">=[</span><span class="s2">&#34;vector-1.yaml&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">2025-08-29T09:05:09.575776Z  INFO vector::topology::running: Running healthchecks.
</span></span><span class="line"><span class="ln">5</span><span class="cl">2025-08-29T09:05:09.575874Z  INFO vector: Vector has started. <span class="nv">debug</span><span class="o">=</span><span class="s2">&#34;false&#34;</span> <span class="nv">version</span><span class="o">=</span><span class="s2">&#34;0.49.0&#34;</span> <span class="nv">arch</span><span class="o">=</span><span class="s2">&#34;aarch64&#34;</span> <span class="nv">revision</span><span class="o">=</span><span class="s2">&#34;dc7e792 2025-08-12 13:47:08.632326804&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">2025-08-29T09:05:09.575918Z  INFO vector::topology::builder: Healthcheck passed.
</span></span><span class="line"><span class="ln">7</span><span class="cl">2025-08-29T09:05:09.575929Z  INFO vector::app: API is disabled, <span class="nb">enable</span> by setting <span class="sb">`</span>api.enabled<span class="sb">`</span> to <span class="sb">`</span><span class="nb">true</span><span class="sb">`</span> and use commands like <span class="sb">`</span>vector top<span class="sb">`</span>.
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="o">{</span><span class="s2">&#34;message&#34;</span>:<span class="s2">&#34;{\&#34;message\&#34;:\&#34;{\\\&#34;_audit_apiVersion\\\&#34;:\\\&#34;audit.k8s.io/v1\\\&#34;,\\\&#34;_audit_objectNamespace\\\&#34;:\\\&#34;default\\\&#34;,\\\&#34;_audit_authorizationReason\\\&#34;:\\\&#34;\\\&#34;,\\\&#34;_audit_requestReceivedTimestamp_date\\\&#34;:\\\&#34;2025-08-29T09:05:13.486642Z\\\[...]&#34;</span><span class="o">}</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="o">{</span><span class="s2">&#34;message&#34;</span>:<span class="s2">&#34;{\&#34;message\&#34;:\&#34;{\\\&#34;_audit_apiVersion\\\&#34;:\\\&#34;audit.k8s.io/v1\\\&#34;,\\\&#34;_audit_objectNamespace\\\&#34;:\\\&#34;kyverno\\\&#34;,\\\&#34;_audit_authorizationReason\\\&#34;:\\\&#34;RBAC: allowed by RoleBinding \\\\\\\&#34;kyverno:reports-controller/kyverno\\\\\\\&#34; of Role[...]&#34;</span><span class="o">}</span>
</span></span></code></pre></div><p><strong>Cool</strong>, j'arrive à récupérer les logs, et vais pouvoir les envoyer sur ma propre stack et/ou ailleurs, au choix !! On va quand même faire un peu de ménage dans le format des messages, y a du redondant. Ce qui nous donne le fichier de conf suivant :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># vector-2.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">sources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">ovh_ldp_source</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">websocket</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">uri</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;wss://gra1.logs.ovh.com/tail/?tk=abcdefghijklmnopqrstuvwxyz&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">decoding</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span><span class="nt">codec</span><span class="p">:</span><span class="w"> </span><span class="l">json</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nt">transforms</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">ovh_ldp_transform</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">remap</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">      </span>- <span class="l">ovh_ldp_source</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="sd">      . = parse_json!(.message)
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="sd">      . = parse_json!(.short_message)</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="nt">sinks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">  </span><span class="nt">stdout_sink</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">console</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">    </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">      </span>- <span class="l">ovh_ldp_transform</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">    </span><span class="nt">encoding</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">      </span><span class="nt">codec</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;json&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">      </span><span class="nt">json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">        </span><span class="nt">pretty</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl">$ vector -c vector-2.yaml
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">2025-08-29T09:20:09.555364Z  INFO vector::app: Log level is enabled. <span class="nv">level</span><span class="o">=</span><span class="s2">&#34;info&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">2025-08-29T09:20:09.559014Z  INFO vector::app: Loading configs. <span class="nv">paths</span><span class="o">=[</span><span class="s2">&#34;vector-2.yaml&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">2025-08-29T09:20:09.787928Z  INFO vector::topology::running: Running healthchecks.
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">2025-08-29T09:20:09.789154Z  INFO vector::topology::builder: Healthcheck passed.
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">2025-08-29T09:20:09.789801Z  INFO vector: Vector has started. <span class="nv">debug</span><span class="o">=</span><span class="s2">&#34;false&#34;</span> <span class="nv">version</span><span class="o">=</span><span class="s2">&#34;0.49.0&#34;</span> <span class="nv">arch</span><span class="o">=</span><span class="s2">&#34;aarch64&#34;</span> <span class="nv">revision</span><span class="o">=</span><span class="s2">&#34;dc7e792 2025-08-12 13:47:08.632326804&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">2025-08-29T09:20:09.790055Z  INFO vector::app: API is disabled, <span class="nb">enable</span> by setting <span class="sb">`</span>api.enabled<span class="sb">`</span> to <span class="sb">`</span><span class="nb">true</span><span class="sb">`</span> and use commands like <span class="sb">`</span>vector top<span class="sb">`</span>.
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="o">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="s2">&#34;annotations&#34;</span>: <span class="o">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="s2">&#34;authorization.k8s.io/decision&#34;</span>: <span class="s2">&#34;allow&#34;</span>,
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="s2">&#34;authorization.k8s.io/reason&#34;</span>: <span class="s2">&#34;RBAC: allowed by RoleBinding \&#34;kyverno:background-controller/kyverno\&#34; of Role \&#34;kyverno:background-controller\&#34; to ServiceAccount \&#34;kyverno-background-controller/kyverno\&#34;&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="o">}</span>,
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="s2">&#34;apiVersion&#34;</span>: <span class="s2">&#34;audit.k8s.io/v1&#34;</span>,
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="s2">&#34;auditID&#34;</span>: <span class="s2">&#34;f861cf50-a561-4ad7-b8f0-8bbb068152d3&#34;</span>,
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="s2">&#34;kind&#34;</span>: <span class="s2">&#34;Event&#34;</span>,
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="s2">&#34;level&#34;</span>: <span class="s2">&#34;Metadata&#34;</span>,
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="s2">&#34;objectRef&#34;</span>: <span class="o">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="s2">&#34;apiGroup&#34;</span>: <span class="s2">&#34;coordination.k8s.io&#34;</span>,
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="s2">&#34;apiVersion&#34;</span>: <span class="s2">&#34;v1&#34;</span>,
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="s2">&#34;name&#34;</span>: <span class="s2">&#34;kyverno-background-controller&#34;</span>,
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="s2">&#34;namespace&#34;</span>: <span class="s2">&#34;kyverno&#34;</span>,
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="s2">&#34;resource&#34;</span>: <span class="s2">&#34;leases&#34;</span>,
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="s2">&#34;resourceVersion&#34;</span>: <span class="s2">&#34;37003755025&#34;</span>,
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="s2">&#34;uid&#34;</span>: <span class="s2">&#34;9fca9fef-d75f-4342-a69f-eada6f93de0f&#34;</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="o">}</span>,
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="o">[</span>...<span class="o">]</span>
</span></span></code></pre></div><p>Ahh, c'est beaucoup mieux 👍 !</p>
<h2 id="la-suite">La suite</h2>
<p>On a pu voir comment récupérer les logs de la &quot;Logs Data Plaform&quot; d'OVHCloud via <code>websocket</code>, grâce notamment à l'outil Vector.</p>
<p>Ca marche très bien en local, maintenant à vous de configurer vos <code>transforms</code> et <code>sinks</code> pour traiter vos logs comme bon vous semble.</p>
<p>De mon côté, c'est déployé via chart Helm en mode <code>Aggregator</code> sur mes clusters, avec quelques <code>transforms</code> nécessaires pour coller avec d'autres logs envoyés dans <a href="https://grafana.com/docs/loki/latest/">Loki</a>.</p>
<p>Amusez-vous bien avec tous ces nouveaux logs !</p>]]></content:encoded></item><item><title>Notes Devoxx France 2024</title><link>https://www.vrchr.fr/posts/2024/04/19/devoxxfr/</link><pubDate>Fri, 19 Apr 2024 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2024/04/19/devoxxfr/</guid><description>Retour sur la conférence DevoxxFR 2024.</description><content:encoded><![CDATA[<p>Devoxx France, c'est <strong>LA</strong> conférence française pour les développeurs et l'écosystème IT. Ayant assisté à <a href="/posts/2022/04/23/devoxxfr-feedback/">l'édition 2022</a> pour les 10 ans, je reviens cette fois en tant que speaker pour cette nouvelle édition. Vous trouverez mes quelques notes de l'événement, qui encore une fois est un événement à ne pas manquer !</p>
<p>On parlera comme d'habitude des conférences, de l'ambiance et mes notes tech ou moins tech. Bonne lecture !</p>
<h2 id="devoxx-france-2024">Devoxx France 2024</h2>
<p>Cette année, <a href="https://www.devoxx.fr/">Devoxx France</a> a vu les choses en grand : alors que les années précédentes la conférence se tenait sur 1 étage au Palais des Congrès, cette fois-ci les organisateurs ont osé rajouter 1 étage supplémentaire pour accueillir plus de monde et plus de conférences : jusqu'à 4500 personnes par jour, pour plus de 250 conférences, tout confondu.</p>
<p>Par rapport à 2022, je remarque 2 changements sur le programme :</p>
<ol>
<li>Il y a des deep-dive &amp; hands-on lab tous les jours, mélangés aux autres conférences plus traditionnelles</li>
<li>On a à chaque fois 30 minutes de pause entre les sujets, le temps d'aller d'une salle à l'autre, et c'est agréable de ne pas devoir courir entre les sessions.</li>
</ol>
<p>Les hands-on &amp; deep-dive étant plus compliqués à suivre en replay, j'ai alors privilégié d'aller voir ce format lorsque j'hésitais. Dur choix vu les conférences de qualité à côté. L'application mobile <a href="https://mobile.devoxx.com/events/devoxxfr2024/schedule">devoxx.com</a> était disponible pour faire son agenda, savoir si les salles étaient pleines, et faire des feedbacks, application toujours utile dans ces moments-là !</p>
<p>Contrairement à la <a href="/posts/2024/03/23/kubecon-eu/">KubeCon</a>, je n'ai pas eu de problème de &quot;full room&quot;, et c'est plutôt agréable.</p>
<p><img alt="Attente de la keynote" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-attente.webp"></p>
<p>Et comme dit en intro, cette année je donne moi-même une conférence, un &quot;Tools-in-Action&quot; sur 2 outils pour vous aider à <a href="https://www.devoxx.fr/schedule/talk/?id=14706">dimensionner correctement vos déploiements sous Kubernetes</a>. On y reviendra 😉</p>
<h2 id="les-keynotes">Les keynotes</h2>
<p>Chaque jour, la journée commence avec 2 keynotes, toujours présentées par <a href="https://twitter.com/aheritier">Arnaud Héritier</a> et <a href="https://twitter.com/karesti">Katia Aresti</a>, qui savent mettre le public de bonne humeur !</p>
<p>Malheureusement, je n'ai pu assister à celle du mercredi, étant dans les transports pour venir, mais je n'ai pas loupé les 2 autres.</p>
<p>Le jeudi, <a href="https://twitter.com/markrendle">Mark Red</a> a d'abord fait le tour de quelques &quot;erreurs&quot; de programmation qui ont pu engendrer des problèmes plus graves qu'un simple core dump, le tout traité avec beaucoup d'humour ! Ensuite, rien à voir avec les bugs, <a href="https://twitter.com/agnes_crepet">Agnès Crepet</a> et <a href="https://twitter.com/GuillaumePitron">Guillaume Pitron</a> nous ont parlé surtout de l'Indium, présent dans tous les écrans tactiles, enfin c'est ce que j'en retiens 😉</p>
<p>Vendredi, j'ai suivi avec un peu plus d'attention <a href="https://twitter.com/AnatoleChouard">Anatole Chouard</a> qui présentait le rapport Meadows, puis Guillaume Poupard qui nous a parlé de cybersécurité, avec beaucoup d'auto-dérision. Cette dernière est sûrement celle que j'ai préférée !</p>
<h2 id="les-conférences-du-mercredi">Les conférences du mercredi</h2>
<p>C'est maintenant parti pour le marathon des confs ! je ne présente les notes que celles auxquelles j'ai assistées, cela ne se veut pas un listing des meilleures confs de l'événement, il y a eu en avoir des beaucoup mieux !</p>
<h3 id="datacenter-as-code--les-arcanes-de-notre-build-system-omniscient-basé-sur-nix">Datacenter-as-Code : les arcanes de notre build system omniscient basé sur Nix</h3>
<p>On commence par une conférence sur <a href="https://nixos.org/">Nix</a>, outil que je suis de près mais dont je n'ai jamais eu d'occasion de mettre en place en &quot;production&quot;.</p>
<p><a href="https://www.linkedin.com/in/nicolas-bernard-83a497b1/">Nicolas Bernard</a> &amp; <a href="https://www.linkedin.com/in/xa-maillard/">Xavier Maillard</a> de chez <a href="https://www.s3ns.io/">S3ns</a> nous présentent donc Nix, et comment ils l'utilisent pour leur infrastructure.</p>
<p>Après une présentation générale, on apprend comment Nix leur est utile pour construire un système consistant, reproductible &amp; co (ce que tout le monde connait de Nix). Mais le plus intéressant, c'est d'avoir un arbre de dépendance complet de dépendance entre tous les éléments de l'infrastructure : dépôts d'artefacts, sources, etc. Et donc de pouvoir mieux &quot;visualiser&quot; les composants d'infrastructure que l'on souhaite gérer.</p>
<p>Les démos très visuelles avec le suivi des dépendances permettent de mieux comprendre l'intérêt de l'outil.</p>
<p><img alt="Datacenter-as-Code : les arcanes de notre build system omniscient basé sur Nix" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-nix.webp"></p>
<h3 id="le-sql-pour-les-nerds">Le SQL pour les nerds</h3>
<p>Même si SQL n'est pas ma tasse de thé, il est toujours bien de voir des sujets avec lesquels on n'est pas le plus à l'aise. <a href="https://twitter.com/l_avrot">Laetitia Avrot</a> nous présente alors comment elle a essayé de résoudre l'Advent Of Code avec seulement du SQL, avec son SGBD favori PostgreSQL, sans extensions. SQL étant un langage Turing-complet, on peut l'utiliser pour ce challenge !</p>
<p>Comme outil, tout sera fait avec <code>psql</code>, parce que c'est simplement le meilleur (autocomplete, historique, aide).</p>
<p>Laetitia nous fait un bilan de ce qu'elle a appris, ce qu'elle en retient (et ce que j'ai noté 😉) :</p>
<ul>
<li>L'advent of code est à faire pour s'amuser. Si vous voulez apprendre à coder avec, ce n'est pas forcément le plus simple, car les exercices deviennent vite complexes (problème d'algo et pas de langage)</li>
<li>L'exercice lui a permis par contre de découvrir des fonctionnalités SQL qu'elle n'utilisait pas</li>
<li>Pas mal de soucis de performance rencontrés, ne pas hésiter à utiliser les nombreux data types dispo ((données géométriques de type points, polygones, etc).</li>
</ul>
<p>Elle en profite pour nous donner quelques tips sur la construction de requêtes SQL : création d'indices partiels (<code>create index where</code>), génération de séries et colonnes, Common Table Expressions. Je la crois sur paroles 😉.</p>
<p><img alt="Le SQL pour les nerds" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-sql.webp"></p>
<p>On peut retrouver le code des exercices sur son dépôt <a href="https://gitlab.com/l_avrot/adventofcode.git">git</a>.</p>
<p>Session instructive, qui confirme que je ne suis pas un DBA 😁.</p>
<h3 id="quality--security-gate-dans-nos-chaines-cicd">Quality &amp; Security Gate dans nos chaines CI⚡CD</h3>
<p>L'équipe de &quot;CATS&quot; (pour Crédit Agricole Technologies &amp; Services) nous font un REX sur la mise en place de &quot;gates&quot; de sécurité dans leurs pipelines de CI/CD, pour délivrer du code de plus en plus sûr.</p>
<p>On retrouve une chaine d'intégration assez standard, du GitLab, Sonarqube, Artifactory, Helm &amp; ArgoCD pour déployer sur des clusters Kubernetes.</p>
<p>Pour aider les développeurs, on note 3 niveaux de maturité des applications (environnements): scratch, staging, stable</p>
<ol>
<li>En scratch : aller le plus vite possible. Jobs en parallèle pour limiter le temps de build.</li>
<li>En staging : cas d'une release : sur création de tag. Même workflow que scratch, mais avec un tag.</li>
<li>En stable : pour la MEP. déclenchement sur une MR.</li>
</ol>
<p>Les analyses de sécurité se font sur 2 axes :</p>
<ol>
<li>SCA : Software Composition Analysis : gestion des dépendances (avec JFrog XRay)</li>
<li>SAST : code vulnérable ? (avec Sonarqube)</li>
</ol>
<p>Au-delà des outils, le rex se fait surtout sur l'activation au fur et à mesure des &quot;gates&quot;, sans bloquer les devs. Toujours une histoire d'accompagnement, et de compromis : où positionner le blocage ? Au plus tôt dans la CI ? Avant la CD ?</p>
<p><img alt="Quality et Security Gate dans nos chaines CI⚡CD" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-cats.webp"></p>
<p>Ce que je note d'intéressant, c'est que les checks se font au fur et à mesure, avec des &quot;tampons&quot;&quot; tout au long du process d'intégration.</p>
<p>Au niveau de la CD, on ne fait &quot;que&quot; vérifier si l'ensemble des &quot;tampons&quot; des gates est OK.</p>
<h3 id="micheline-120-ans-est-passée-à-liac">Michelin(e), 120 ans, est passée à l'IaC</h3>
<p>Michelin est un client de ma société, et je les ai découverts lors de projets SRE &amp; Observabilité, et j'ai été bluffé par l'équipe tech, leur maturité &amp; leur humilité. Je me devais d'aller voir ce talk.</p>
<p>Jouant un jeu de ping-pong, <a href="https://twitter.com/tioumen">Guillaume Hospital</a> et <a href="https://twitter.com/murdockk59">Adrien Gooris</a> nous présentent comment les équipes ont évolués sur la gestion de l'infra, périple que nombreuses sociétés ont vécu avec plus ou moins de succès.</p>
<p>On démarre avec les fameux tickets pour créer des environnements jamais ISO, la mise en place d'outils &amp; de process, migration vers le cloud, pour terminer sur du platform engineering &amp; portail self service. Belle histoire racontée avec les challenges, les succès &amp; échecs. Un rex toujours intéressant !</p>
<p><img alt="Michelin(e), 120 ans, est passée à l'IaC" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-michelin.webp"></p>
<h3 id="les-équipes-distribuées-ça-peut-marcher">Les équipes distribuées, ça peut marcher?</h3>
<p>On change des REX techniques, un sujet plutôt organisationnel cette fois !</p>
<p><a href="https://twitter.com/bdelacretaz">Bertrand Delacretaz</a>, fait un tour d'horizon des solutions pour travailler facilement avec des équipes distribuées. Ayant été longtemps dans la fondation Apache, il nous donne pas mal d'exemples.</p>
<p>On commence d'abord par ce qui fait mal : le temps perdu, en réunion (souvent peu productives) coûtent cher et doivent être &quot;précieuses&quot;, le 'maker schedule' vs le 'manager schedule' (un artisan qui colle une chaise ne peut être interrompu, sinon il doit tout refaire - le dev c'est presque pareil).</p>
<p>Il propose alors plusieurs solutions pour éviter des réunions inutiles, j'en ai noté 2 :</p>
<ul>
<li>les décisions asynchrones : on discute en asynchrone avant, on prépare &amp; on choisit. On ne se réunit que pour valider la décision.</li>
<li>on utilise un canal partagé pour échanger / annoncer les décisions</li>
</ul>
<p>Bien sûr, il faut des outils : avant, la Mailing List était très pratique, si bien utilisée. Maintenant on utilise surtout des issues / tickets. Attention néanmoins au contenu des messages :</p>
<ol>
<li>Soyez clair dans le message. On écrit pour un public, pas pour soi.</li>
<li>Attention à la lecture multi niveaux &amp; différence de niveau de langage : soyez concis.</li>
<li>Exprimez vos intentions, plutôt que de demander l'autorisation (exemple des Merges Requests)</li>
</ol>
<p>Enfin, il parle du &quot;problème&quot; de la multi-culture : Utilisez un langage simple, phrases courtes, pour éviter les confusions et effets de style. Partez du principe que l'interlocuteur a été bienveillant.</p>
<p>Tout cela reste du bon sens, mais c'est toujours bien de le rappeler !</p>
<p><img alt="Les équipes distribuées, ça peut marcher?" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-equipes-distris.webp"></p>
<h3 id="neovim-en-2024">(Neo)Vim en 2024</h3>
<p>Premier &quot;Tools-In-Action&quot; de cette année ! Utilisateur de vim, et aussi vscode quand j'ai la flemme, je me dis toujours que je devrais prendre le temps de basculer sur NeoVim. Justement une session dessus, c'est l'occasion !</p>
<p>Swann Castel nous présente alors l'outil, et ses différences avec Vim. On notera le support natif des LSP (Language Server Support), les plugins manager comme &quot;Lazy&quot;, Treesitter et autres plugins (telescope).</p>
<p>Il nous confie qu'avoir une configuration NeoVim peut prendre du temps, mais qu'il existe des solutions prêtes à l'emploi (lazyvim, astronvim).</p>
<p>Même si je comprends l'intérêt de l'outil, je reste un peu sur ma faim, ayant eu un peu de mal à voir la vraie plus-value par rapport à ce que j'utilise aujourd'hui. Il faut <strong>vraiment</strong> que je passe un peu de temps sur un setup !</p>
<h3 id="dimensionnez-correctement-vos-déploiements-kubernetes">Dimensionnez correctement vos déploiements Kubernetes</h3>
<p>Dernier slot de la journée de mercredi, c'est à mon tour !!</p>
<p>Je présente 2 outils pour aider les devs à dimensionner leur déploiement sur Kubernetes (requests &amp; limits).</p>
<p>Pour la dernière conf de la journée, la salle est pleine, le public réceptif et les démos se déroulent sans accrocs !</p>
<p>Très satisfait de la présentation et de l'accueil du &quot;public&quot;, bons feedbacks après la conf et tout le reste de l'événement, ça rebooste !!</p>
<p>Pour celles &amp; ceux ayant manqué, ma présentation est disponible <strong><a href="https://presentations.verchere.fr/Requests_Limits_Devoxxfr_2024/">ici</a></strong>.</p>
<p><em>Note</em> : j'ai bien galéré pour le CSS des slides (oui c'est du HTML grâce à Marp !).</p>
<h2 id="les-conférences-du-jeudi">Les conférences du jeudi</h2>
<p>la première journée était bien remplie, cette seconde le sera un peu moins, j'assiste a des sessions plus longues 😉.</p>
<h3 id="gatewayapi-10-ans-de-maturation-pour-une-nouvelle-api-kubernetes">GatewayAPI, 10 ans de maturation pour une nouvelle API Kubernetes</h3>
<p><a href="https://twitter.com/davinkevin">Kevin Davin</a> de chez Gradle présente la nouvelle API disponible dans kubernetes, &quot;Gateway API&quot;, qui permet de résoudre certaines limitations que nous pouvons avoir les ressources Ingress.</p>
<p>Les ingress ne sont apparus dans Kubernetes que depuis la version 1.10, et sont pensés HTTP first. Avec l'utilisation de gRPC et des flux plus classiques en TCP, la gestion des certificats TLS and co, l'ingress se voit limité. On a aujourd'hui 22 Ingress Controlers avec des implémentations différentes, ça devient compliqué d'avoir quelque chose de commun.</p>
<p>La Gateway API est alors définie comme spécification pour prendre le problème dans son intégralité, avec 4 concepts :</p>
<ul>
<li>Orienté par rôle</li>
<li>Portabilité : guide pour implémenter la spec</li>
<li>Extensibilité : pas de CRD DANS k8s, donc possible d'installer sur n'importe quelle version de k8s</li>
<li>Expressif : HTTPRoute, TCPRoute, etc.. On sait quel protocole c'est</li>
</ul>
<p>On a donc plusieurs ressources, qui seront utilisées par un public différent (administrateur infra, sécu, développeur) :</p>
<ul>
<li><strong>GatewayClass</strong> : Plusieurs définitions en fonction de l'usage : mono/multicluster par exemple.</li>
<li><strong>Gateway</strong> : instancie une GatewayClass. Chaque gateway peut créer un LB, car défini dans la GatewayClass. Expose les ports à l'extérieur. Gérées plutôt par les Ops.</li>
<li><strong>Routes</strong> : Possibilité de s'accrocher à plusieurs gateways, par ex 2 url interne/externe, avec filtres &amp; co. Gérées plutôt par les Devs.</li>
</ul>
<p>On notera des fonctionnalités avancées comme la règle sur les PATHs, ler verbes (GET, PUT), les headers, la possibilité de faire du mirroring de flux (RequestMirror), spltting (canary release).</p>
<p><img alt="GatewayAPI, 10 ans de maturation pour une nouvelle API Kubernetes" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-gateway-api.webp"></p>
<p>Conférence super intéressante donnant un aperçu complet de la spec et son utilisation. Je crois une de mes sessions préférées !</p>
<h3 id="au-cœur-de-la-ruche-ebpf">Au cœur de la ruche eBPF!</h3>
<p>Allez un peu de réseau et sécurité avec eBPF ! On commence à connaître la techno avec les équipes d'Isovalent et leur CNI Cilium, mais je voulais avoir une présentation moins biaisée, ici fait par <a href="https://twitter.com/laytoun">Mohammed Aboullaite</a> de chez Spotify.</p>
<p>Mohammed donne une présentation claire et assez simple (et en français !) de la technologie, avec des démos pour appuyer ses exemples. Le code présenté était en Python, avec des librairies qui vont bien pour appeler du code eBPF. Côté Java, à noter le projet &quot;<a href="https://openjdk.org/jeps/454">panama</a>&quot;.</p>
<p><img alt="Au cœur de la ruche eBPF!" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-ebpf.webp"></p>
<p>J'ai noté quelques remarques intéressantes :</p>
<ul>
<li>&quot;eBPF, tout le monde en parle, mais peu développent avec&quot;</li>
<li>&quot;eBPF pour le kernel, c'est comme Javascript pour le browser&quot;</li>
</ul>
<h3 id="pulumi-ou-comment-gérer-votre-infrastructure-avec-votre-langage-préféré">Pulumi, ou comment gérer votre infrastructure avec votre langage préféré</h3>
<p>Un deep dive de <strong>3 heures</strong> sur Pulumi, présenté par le trio <a href="https://twitter.com/ju_hnny5">Julien Briault</a>, <a href="https://twitter.com/idriss_neumann">Idriss Neumann</a> et <a href="https://twitter.com/ElFamosoKwak">Alexis Fala</a>, il fallait voir cela ! La techno m'intéresse, et je voulais voir comment cela pouvait être mis en pratique &quot;en vrai&quot;, avec les limites que cela peut avoir.</p>
<p>On commence par une introduction à l'IaC, au DevOps, et le besoin d'avoir des outils comme Ansible, Terraform pour industrialiser le déploiement d'infrastructure.</p>
<p>Pulumi est arrivé en 2019, avec une approche orienté développeur, qui permet de coder son infra avec le langage de son choix, avec sa puissance (boucles, conditions, héritage, intégration avec d'autres libs, etc.).</p>
<p>Après une comparaison avec Terraform et ses limites, l'équipe de speaker nous fera quelques démos, pour enfin arriver à un cas d'usage très concret sur la plateforme d'Idriss, <a href="https://www.comwork.io/fr/comwork-cloud">Comwork Cloud</a>.</p>
<p><img alt="Pulumi, ou comment gérer votre infrastructure avec votre langage préféréumi" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-pulumi.webp"></p>
<p>Les 3 heures sont vite passées, les explications claires et argumentées (pas mal de questions, des réponses pertinentes). 2 remarques mineures, la pause est arrivée très tard, et Idriss n'a pas été assez clair sur la transition Pulumi &amp; sa plateforme, certains ont pu perdre le fil (on en a discuté après, un oubli dû au stress !).</p>
<p>Maintenant, vais-je l'utiliser ? Pas sûr, je suis très typé &quot;infra&quot;, j'ai encore du mal à me dire qu'il faut coder pour déployer de l'infra. À suivre...</p>
<h3 id="orchestrer-ses-workflows-métiers-avec-temporal">Orchestrer ses workflows métiers avec Temporal</h3>
<p>Après 3 heures d'infra as code, quoi de mieux que de l'orchestration de workflows as code ??</p>
<p>Et bien c'est fait avec Nathan Castelein de chez Shodo qui nous présente Temporal.</p>
<p>Présentation originale du concept en faisant l'analogie d'une chanson de Miley Cyrus (déso mais on n'a pas du tout les mêmes goûts musicaux).</p>
<p>Après une définition des concepts de workflows et orchestrateurs, on parle de Temporal : orchestrateur de workflows créé par Uber en 2020, avec de gros contributeurs comme Datadog, Strip, OVHCloud, Hashicorp.</p>
<p>Temporal introduit 4 concepts :</p>
<ul>
<li>Workflows</li>
<li>Activities</li>
<li>Retry Policies</li>
<li>Workers</li>
</ul>
<p>Quelques notes brutes :</p>
<ul>
<li>Les workflows sont &quot;as code&quot;, peuvent être développés en go, mais pas que.</li>
<li>Si une activité se termine bien 1 fois, elle n'est jamais ré-exécutée</li>
<li>Au final, du code avec un peu de &quot;sucre&quot; autour pour gérer la robustesse des workflows.</li>
</ul>
<p><img alt="Orchestrer ses workflows métiers avec Temporal" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-temporal.webp"></p>
<h3 id="créer-un-meetup--mais-quelle-drôle-didée-">Créer un meetup ? mais quelle drôle d'idée !</h3>
<p>Dernier meeting de la journée, un BOF avec l'équipe de <a href="https://www.tadx.fr/">TADx</a> sur comment créer un meetup et le gérer.</p>
<p>Cet échange était plutôt là pour savoir comment chacun gérer les meetups, la plupart des gens présents étant organisateurs (moi-même ayant rejoint l'équipe d'orga du meetup <a href="https://www.meetup.com/devops-aix-marseille/">DevOps Aix-Marseille</a>).</p>
<p>Au travers d'un jeu de questions réponses, ont été abordés divers thèmes comme les sujets de meetups, leur fréquence, les lieux, le financement, tech ou pas tech, du sponsoring d'entreprises, etc.</p>
<p>Je note pour moi surtout la création d'asso qui peut éviter des problèmes d'assurance.</p>
<p>Échanges bon enfant, avec des avis divers et toujours constructifs (promis un jour je viendrai à TADx !) !</p>
<h2 id="les-conférences-du-vendredi">Les conférences du vendredi</h2>
<p>Dernière journée, et ce n'est pas encore fini ! Comme jeudi, je vais privilégier des longues sessions, désolé pour les autres !</p>
<h3 id="serverless--éviter-le-vendor-locking-en-construisant-sa-propre-plateforme-faas-avec-openfaas-et-kubernetes">Serverless : Éviter le vendor locking en construisant sa propre plateforme FaaS avec OpenFaaS et Kubernetes</h3>
<p>Après les keynotes, on démarre la journée avec <a href="https://twitter.com/katia_tal">Katia Himeur</a> pour 2 heures de workshops sur du Serverless.</p>
<p>Une rapide introduction sur ce qu'est le serverless, Katia nous présente l'outil Openfaas, comment l'utiliser, ses atouts &amp; limites.</p>
<p>C'est un lab, donc on enchaine les exercices :</p>
<ul>
<li>installation d'openfaas</li>
<li>création d'une première fonction en python</li>
<li>déploiement d'une 'stack'</li>
<li>exploration d'openfaas avec la gestion des substitutions, des secrets, etc.</li>
<li>utilisation avec du message queuing RabbitMQ pour déclencher des fonctions</li>
</ul>
<p><img alt="Serverless : Éviter le vendor locking en construisant sa propre plateforme FaaS avec OpenFaaS et Kubernetes" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-openfaas.webp"></p>
<p>Alors, accrochez-vous, un workshop avec Katia ça va viiite, il faut suivre ! Sujet intéressant, mais on a pu voir quelques limites à openfaas, la prochaine fois on regardera <a href="https://github.com/AxaFrance/SlimFaas">SlimFaas</a> !</p>
<p>Vous retrouverez les exercices du lab <a href="https://gitlab.com/cockpit-io-public/openfaas-workshop">ici</a> (Attention, certains exercices ne passent que sur Mac, à cause des images docker arm).</p>
<h3 id="des-silos-au-platform-engineering-en-passant-par-le-devops--adopter-gitops-et-aller-au-delà-de-lhype">Des silos au Platform Engineering en passant par le DevOps : adopter GitOps et aller au-delà de l’hype</h3>
<p>Encore un deep-dive de 3 heures pour parler DevOps, GitOps &amp; Platform Engineering... J'avoue que j'ai hésité, mais <a href="https://twitter.com/LostInBrittany">Horacio González</a> m'a fortement suggéré de venir... Et je n'ai pas été déçu !</p>
<p>Le duo retrace avec humour et auto-dérision les différentes périodes des Mises en Production, du fichier à télécharger en FTP sur un serveur jusqu'à ce qu'il se fait aujourd'hui, personne n'est épargné : les devs, les ops, les devops, les agilistes. Bref on se régale à les écouter et on se retrouve forcément dans un des personnages qu'ils décrivent.</p>
<p>Après de longues tirades que je n'oublierai pas (&quot;Kubernetes c'est comme le stagiaire&quot;), on aborde les sujets d'opérateurs kubernetes comme bon pattern pour le Platform Engineering, et <a href="https://twitter.com/sebi2706">Sébastien Blanc</a> terminera la session par une démo de ce qui se fait aujourd'hui en termes de déploiement, à base d'ArgoCD et de <a href="https://www.kratix.io/">Kratix</a></p>
<p>Et, cerise sur le gâteau, je suis un peu leur fil conducteur dans leur présentation avec une phrase maintenant devenue culte (au moins pour moi) : &quot;Bash will still be used&quot;.</p>
<p><img alt="Bash will still be used" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-bash.webp"></p>
<h3 id="tout-ce-que-vous-avez-toujours-voulu-savoir-sur-les-cni-container-network-interface-dans-kubernetes">Tout ce que vous avez toujours voulu savoir sur les CNI (Container Network Interface) dans Kubernetes</h3>
<p>Après autant de workshops &amp; deep-dive, vous pensiez que j'allais m'arrêter là ? Et bien non, encore un tools-in-action à voir avc <a href="">Mickael Roger</a> qui nous explique comment ça marche une CNI !</p>
<p>Cette session n'est pas une présentation des diverses CNI (Calico, Cilium, etc.), mais vraiment une explication de comment ça fonctionne, &quot;en vrai&quot;.</p>
<p>la présentation est didactique, avec un pas à pas de ce qu'il se passe sur vos nodes lorsqu'un pod démarre.</p>
<p>Bon, j'avoue il commençait à être tard, je devrais regarder la session en replay, et dommage que Mickaël se soit un peu emmêler les pinceaux sur une démo (lui aussi devait être fatigué :)).</p>
<p><img alt="Tout ce que vous avez toujours voulu savoir sur les CNI (Container Network Interface) dans Kubernetes" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-cni.webp"></p>
<h2 id="les-à-coté">Les &quot;à-coté&quot;</h2>
<p>Biensûr, DevoxxFR ce n'est pas &quot;que&quot; des conférences, ce sont aussi des stands de partenaires, du networking, des discussions, des verres &amp; des goodies !</p>
<p>Côté stand, j'ai passé pas mal de temps avec les copains de R2Devops, discuté avec les gens d'OVHCloud, S3ns, Clever Cloud, Michelin, et évidemment l'équipe d'Accenture Clermont-Ferrand qui était là en force. Il faudra que je me motive pour les prochains événements d'aller voir les sociétés que je connais moins bien...</p>
<p>Le jeudi, c'est Meet &amp; Greet en plus des &quot;BOF&quot;, et j'ai eu le plaisir de rencontrer IRL certaines personnes avec qui j'échange sur les réseaux (coucou <a href="https://twitter.com/MazlumTosun3">Mazlum</a> 😙), squatter un peu sur le stand de S3ns avec les clones de <a href="https://twitter.com/guilhemlettron">Guilhem</a> 🧥 et leur tireuse 🍻. Je ne peux pas nommer tout le monde, ce post est déjà bien trop long 😁.</p>
<p>Enfin, j'ai découvert le Canadian Embassy Pub, passage obligé après la conférence, où l'on débriefe jusque tard dans la soirée (coucou <a href="https://twitter.com/GwinizDu">Steven</a> &amp; <a href="https://twitter.com/laurentgrangeau">Laurent</a> 😇) !</p>
<h2 id="bilan">Bilan</h2>
<p>Encore 3 jours très enrichissants, tant au niveau technique qu'humain !</p>
<p>D'un côté, j'aime toujours découvrir de nouveaux sujets, ou confronter mes avis / expériences pour découvrir de nouveaux usages, des solutions ou alternatives à des problèmes que j'ai pu rencontrer. En tant que speaker, c'est aussi l'occasion de transmettre mon expérience à celles &amp; ceux qui viennent. C'est donc gagnant-gagnant.</p>
<p>D'un autre côté, c'est un booster niveau relations sociales, on rencontre de nouvelles têtes, on croise des habitués, des collègues (anciens, nouveaux, futurs), et lorsqu'on est en full télétravail comme moi c'est toujours agréable de discuter &quot;IRL&quot; avec nos pairs.</p>
<p>Bilan donc très positif, à refaire très probablement !</p>
<p><img alt="Stands DevoxxFR vides" class="zoomable" decoding="async" loading="lazy" src="/2024/04/2024-04-19-devoxxfr-fin.webp"></p>
<h2 id="dautres-en-parle">D'autres en parle...</h2>
<p>Voici ci-après d'autres articles de blog sur l'événement, n'hésitez pas à me contacter pour rajouter le vôtre !</p>
<ul>
<li><a href="https://yannmoisan.com/devoxx-france-2024-jour-1.html">Devoxx France 2024 - Jour 1</a> - Yann Moisan</li>
<li><a href="https://yannmoisan.com/devoxx-france-2024-jour-2.html">Devoxx France 2024 - Jour 2</a> - Yann Moisan</li>
<li><a href="https://yannmoisan.com/devoxx-france-2024-jour-3.html">Devoxx France 2024 - Jour 3</a> - Yann Moisan</li>
<li><a href="https://www.olivierthierry.com/devoxx-france-2024/">Mon Devoxx France 2024</a> - Olivier Thierry</li>
<li><a href="https://riduidel.wordpress.com/2024/04/19/devoxx-2024/">Devoxx 2024 ?</a> - Nicolas Delsaux</li>
<li><a href="https://www.adelean.com/blog/20240420_adelean_devoxx_france_2024/">Devoxx France - la conférence des développeurs - cette année sous le signe de l'intélligence artificielle generative</a> - Lucian Precup (Adelean)</li>
</ul>]]></content:encoded></item><item><title>Notes KubeCon &amp; CloudNativeCon Europe 2024</title><link>https://www.vrchr.fr/posts/2024/03/23/kubecon-eu/</link><pubDate>Sat, 23 Mar 2024 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2024/03/23/kubecon-eu/</guid><description>Retour sur la conférence KubeCon &amp;amp; CloudNativeCon Europe 2024.</description><content:encoded><![CDATA[<p>Chaque année, la CNCF organise un événement européen autour des technologies Kubernetes &amp; Cloud Native. Pour 2024, cela se passe en France, à Paris, au Parc des Expositions Porte de Versailles.</p>
<p>C'est une occasion à ne pas manquer étant donné que ce sont des technos sur lesquelles je travaille au quotidien, et surtout le moment de rencontrer partenaires, collègues, amis &amp; l'ensemble de la communauté tech FR.</p>
<p>Comme nombreux &quot;frenchies&quot;, j'ai tenté ma chance en postulant au CFP, et comme la quasi-majorité, je n'y suis allé qu'en simple visiteur ;)</p>
<p>Voici ci-dessous mes notes, on parlera en vrac : des keynotes &amp; conférences, de l'événement, des rencontres... Bonne lecture !</p>
<h2 id="kubetrain--avant-goût">KubeTrain &amp; Avant-Goût</h2>
<p>La KubeCon en elle-même dure 3 jours, mais d'autres conf sont organisées autour : les <a href="https://cloud-native.rejekts.io/">Rejekts</a> le week-end précédent, les collocated-event la veille, une journée pour les enfants le samedi... Ne pouvant me permettre une absence trop longue, je n'ai assisté qu'aux 3 jours &quot;classiques&quot; (et c'est déjà énorme !).</p>
<p>Pour s'y rendre, j'ai profité de l'organisation <a href="https://kubetrain.io/">Kubetrain</a>, qui permet aux gens de venir en train. Un départ était prévu depuis Aix-Marseille, grâce à <a href="https://kubetrain.io/departures/aix-marseille/">Frédéric Léger</a>, merci à lui !</p>
<p><img alt="Kubetrain" class="zoomable" decoding="async" loading="lazy" src="/2024/03/2024-03-24-kubetrain.webp"></p>
<p>Arrivé dans en début de soirée, j'ai pu rejoindre l'équipe OVHCloud &amp; Suse, et voir les quelques personnes sur place. L'occasion également un échange de bon procédé avec <a href="https://twitter.com/nunixtech">Nuno</a> ;)</p>
<p><img alt="Chocolate Corsair" class="zoomable" decoding="async" loading="lazy" src="/2024/03/2024-03-24-chocolats-nuno.webp"></p>
<h2 id="la-découverte-les-keynotes-les-tendances">La découverte, les keynotes, les tendances</h2>
<p>Le jour J, on nous conseille d'arriver tôt pour ne pas perdre du temps au niveau des badges... 7h30 me voilà déjà en place, avec quelques collègues lève-tôt !</p>
<p><img alt="Récupération des badges" class="zoomable" decoding="async" loading="lazy" src="/2024/03/2024-03-24-badges.webp"></p>
<p>Je ne connais pas l'endroit, c'est simplement immense. On nous annonce plus de 12000 personnes, et pourtant ça passe &quot;large&quot;. On retrouve au 1er étage 4 grandes salles, et de grandes tables pour se poser. Au 2nd se trouve l'espace sponsors / exposants, au 3ème la grande salle pour les keynotes &amp; le reste des conférences. Et enfin, le &quot;rooftop&quot; où se retrouvaient régulièrement les frenchies pour debrief ;)</p>
<p>J'ai eu beaucoup de mal à me repérer dans ce vaste espace, surtout au niveau des exposants, ce qui m'a parfois joué des tours (quelques conférences loupées - mauvais endroit, mauvais timing).</p>
<h3 id="les-keynotes">Les keynotes</h3>
<p>Chaque jour, la journée débute par plusieurs keynotes. Des news de la CNCF, des talks sponsors, des sujets de la communauté.</p>
<h4 id="mercredi"><a href="https://www.cncf.io/blog/2024/03/20/kubecon-cloudnativecon-europe-2024-day-two-how-cloud-native-is-powering-the-ai-movement-and-other-news/">Mercredi</a></h4>
<p>Le <strong>mercredi</strong>, c'est <strong>AI</strong> ! Beaucoup de &quot;buzzword&quot; autour de l'AI, GenAI, GPU, LLM &amp; co, assez étrange pour un 1er contact avec la KubeCon. Pourtant, ce que l'on comprend, c'est que l'intelligence artificielle fait partie de notre quotidien, et que Kubernetes est aujourd'hui la plateforme de-facto pour permettre des usages à l'échelle.</p>
<p>On retrouvera plus tard pas mal de conférences sur le sujet, notamment sur comment exploiter au mieux les GPUs dans un environnement Cloud Natif. À suivre, car mon client actuel a justement des problèmes de gestion de partage de GPUs pour ses utilisateurs.</p>
<p><img alt="Keynote" class="zoomable" decoding="async" loading="lazy" src="/2024/03/2024-03-24-keynote1.webp"></p>
<h4 id="jeudi"><a href="https://www.cncf.io/blog/2024/03/21/kubecon-cloudnativecon-europe-2024-day-three-the-power-of-sustainable-computing/">Jeudi</a></h4>
<p>Le <strong>jeudi</strong>, c'est <strong>sustainability</strong> (et <strong>end-users</strong>, mais ça rime moins bien) ! Faire de la tech c'est bien, mais la faire de manière écoresponsable c'est mieux !</p>
<p>Après quelques retours sur l'utilisation des produits CNCF par les utilisateurs (Backstage en pole position), échanges &amp; témoignages pour faire de la techno plus &quot;verte&quot;. Je retiens notamment :</p>
<ul>
<li>La montée en puissance de  WASM, pour avoir des workloads beaucoup plus légers.</li>
<li>La consolidation des applications VM &amp; Conteneurs avec des solutions comme KubeVirt.</li>
<li>La meilleure utilisation des ressources avec tout ce qui tourne autour de l'autoscaling &amp; un meilleur monitoring.</li>
</ul>
<p><img alt="Enduser Commits" class="zoomable" decoding="async" loading="lazy" src="/2024/03/2024-03-24-enduser.webp"></p>
<h4 id="vendredi"><a href="https://www.cncf.io/blog/2024/03/22/kubecon-cloudnativecon-europe-2024-day-four-how-cloud-native-is-powering-the-ai-movement-and-other-news/">Vendredi</a></h4>
<p>Le <strong>vendredi</strong>, c'est <strong>community</strong> ! Je n'ai malheureusement pas pu y assister, mais grâce aux canaux Slack, Telegram &amp; co de la communauté FR j'ai pu suivre de loin quelques annonces :</p>
<ul>
<li>L'annonce du programme #Kubestronautes, qui récompensent les certifiés ayant l'ensemble des certifications CKx. Bravo à mon copain <a href="https://twitter.com/DavAuff">David</a> qui en fait partie !</li>
<li>Les <a href="https://contribcard.clotributor.dev/">contributor card</a></li>
<li>L'intervention de <a href="https://twitter.com/solomonstre">Solomon Hykes</a> avec une très belle référence au cinéma FR ;)</li>
</ul>
<p><img alt="Kubestronautes" class="zoomable" decoding="async" loading="lazy" src="/2024/03/2024-03-24-kubestronautes.webp"></p>
<p>Côté tendances, on l'aura compris, c'est <strong>AI</strong> ! Viennent ensuite tout ce qui tourne autour du <strong>Platform Engineering</strong> &amp; DevX : Kubernetes devient une commodité pour gérer les besoins métiers. <strong>WASM</strong> est à suivre de près, avec <strong>eBPF</strong> qui explose, ainsi que la partie <strong>Service Mesh</strong> côté réseau.</p>
<h2 id="la-communauté-les-partenaires--la-tech-fr">La communauté, les partenaires &amp; la tech FR</h2>
<p>Pour reprendre les mots de la keynote de Solomon, <em>&quot;la vie c'est d'abord des rencontres&quot;</em>, la KubeCon c'est l'occasion de rencontrer les acteurs de la tech, qui plus est française, et faire le tour des stands pour <del>récupérer des goodies</del> découvrir les projets.</p>
<p><img alt="ClashLoopBackoff" class="zoomable" decoding="async" loading="lazy" src="/2024/03/2024-03-24-clashloopbackoff.webp"></p>
<p>Avec plus de 200 exposants, impossible d'échanger avec tout le monde, alors en vrac quelques rencontres :</p>
<ul>
<li>L'espace <strong>projets CNCF</strong>, où étaient représentés les principaux projets, avec leurs mainteneurs. Je n'ai pas passé assez de temps ici, je pense qu'une prochaine fois je privilégierai ce coin. J'ai juste eu l'occasion d'échanger avec les mainteneurs de KubeVirt.</li>
<li>L'équipe de <strong>Rancher by Suse</strong>, où j'ai pu enfin <em>toucher</em> <a href="https://twitter.com/devprofr">Bertrand Thomas</a>, avec qui je partage pas mal d'idées, et qui est comme moi ambassadeur R2Devops. J'ai eu droit à une démo de leur solution edge, pluggée avec <a href="https://github.com/project-akri/akri">akri</a> pour gérer des devices type imprimantes.</li>
<li>L'équipe de <strong>Docker</strong>, où j'ai pu échanger avec <a href="https://twitter.com/glours">Guillaume Lours</a>, qui cherche des retours d'expérience sur la migration Docker Compose vers Kubernetes. Si vous êtes dans ce cas-là, n'hésitez pas à le contacter !</li>
<li>L'équipe <strong>Isovalent</strong>, notamment <a href="https://twitter.com/raphink">Raphaël Pinson</a>, qui proposaient des labs sur l'ensemble des solutions autour de Cilium. Cilium &amp; eBPF qui avaient beaucoup le vent en poupe lors de cette conférence</li>
<li>L'équipe de <strong><a href="https://cast.ai/">Cast.ai</a></strong>, avec <a href="https://www.linkedin.com/in/ferreolhoppenot/">Ferréol Hoppenot</a>, qui nous propose des outils permettant d'optimiser les coûts de vos infra. Un peu comme Karpenter, mais en mieux ;)</li>
<li>Les équipes d'<strong>Athéo</strong> &amp; <strong>Cloudeo</strong>, originaires de l'Est de la France, où <a href="https://www.linkedin.com/in/nicolas-streng/">Nicolas Streng</a> &amp; <a href="https://www.linkedin.com/in/lgromb/">Louis Gromb</a> ont osé venir s'exposer, et ont tout simplement cartonné, bravo à eux !</li>
</ul>
<p>J'ai également pris un peu de temps pour déambuler autour des stands et me laisser &quot;approcher&quot; par les éditeurs, en féliciter d'autres pour leurs produits. Me vient en tête <a href="https://amplication.com/">Amplification</a>, <a href="https://home.robusta.dev/">Robusta</a>, <a href="https://logz.io/">Logz.io</a>.</p>
<p>Niveau employeur, c'était aussi l'occasion de voir &quot;en vrai&quot; les collègues que je ne vois que par webcam !</p>
<p>L'occasion aussi d'échanger avec <a href="https://www.linkedin.com/in/christophesauthier/">Christophe</a> qui gère maintenant toute la partie formation et certification au sein de la CNCF, et rencontrer l'équipe de son ancienne boite Objectif Libre.</p>
<p>Évidemment, j'ai discuté avec d'autres anciens collègues, et d'autres &quot;copains &amp; copines de conf&quot; que je croise régulièrement sur les salons, ne m'en voulez pas si je ne vous cite pas ;)</p>
<p>Enfin, on ne peut pas parler de la communauté FR sans mentionner le <strong>rooftop</strong>, coin très apprécié des frenchies pour prendre l'air et se retrouver, avec une dédicace à la team <a href="https://www.twitch.tv/cuistops">CuistOps</a> que l'on retrouve le lundi soir en live autour des sujets Cloud Native !</p>
<p><img alt="Rooftop - Crédits Q. Joly" class="zoomable" decoding="async" loading="lazy" src="/2024/03/2024-03-24-rooftop.webp"></p>
<h2 id="détails-des-conférences">Détails des conférences</h2>
<p>Pendant 4 jours, je n'ai pas fait <em>que</em> discuter, j'ai quand même pris le temps d'assister à quelques conférences.
Avec un programme très dense, difficile de faire des choix. Si au départ j'ai été un peu déçu de quelques sujets où je n'apprends rien de &quot;nouveau&quot;, j'ai vite changé mon fusil d'épaule et suis allé voir des sujets que je connais moins, pour mieux élargir mes horizons.</p>
<p>Je ne vais pas ci-après faire le détail de chaque conf, vous aurez plus vite fait de voir les replays. Simplement quelques notes sur les sujets qui ont pu me marquer, avec une 🌟 pour mes sessions préférées.</p>
<ul>
<li>
<p><a href="https://kccnceu2024.sched.com/event/1YeLc/architecting-resilience-lessons-from-managing-7k-kubernetes-clusters-at-scale-kwanghun-choi-gyutae-bae-kakao">Architecting Resilience: Lessons from Managing 7K+ Kubernetes Clusters at Scale</a>.
Avoir 1 cluster par zone peut poser des problèmes de dispo lorsqu'on, surtout lorsque tout n'est pas répliqué à 100%.
Passage à une architecture multizone, mais problème de latence réseau à prendre en considération : ETCD, passage à full eBPF, meilleure configuration des GLSB.</p>
</li>
<li>
<p><a href="https://kccnceu2024.sched.com/event/1YeLy/bloombergs-journey-to-a-multi-cluster-workflow-orchestration-platform-yao-lin-reinhard-tartler-bloomberg">Bloomberg's Journey to a Multi-Cluster Workflow Orchestration Platform</a>.
Problèmes de synchro de workflows sur plusieurs clusters.
Pour résoudre cela, utilisation de <a href="https://github.com/k3s-io/kine">kine</a> avec BDD postgres H-A en backend.
À noter que Kine a été présenté lors d'une autre session, à suivre !</p>
</li>
<li>
<p><a href="https://kccnceu2024.sched.com/event/1Yhfc/supercharging-argo-cds-manifest-generation-capabilities-alexander-matyushentsev-akuity-leonardo-luz-almeida-intuit">Supercharging Argo CD’s Manifest Generation Capabilities</a>.
Vous voulez utiliser d'autres trucs que kustomize, helm avec ArgoCD ? Utilisez les ConfigManagementPluginV2 !</p>
</li>
<li>
<p><a href="https://kccnceu2024.sched.com/event/1YhgI/chart-your-course-like-a-champion-andrew-block-karena-angell-red-hat-joe-julian-stealth-startup-scott-rigby-independent">Chart Your Course Like a Champion</a>.
Quelques astuces pour bien écrire les charts, mais <strong>surtout</strong> pour qu'ils soient bien maintenableS (ajout d'infos dans <code>Chart.yaml</code>).</p>
</li>
<li>
<p>🌟 <a href="https://kccnceu2024.sched.com/event/1Yhgs/intro-deep-dive-kubernetes-sig-scalability-wojciech-tyczynski-google-shyam-jeedigunta-amazon-web-services">Intro + Deep Dive: Kubernetes SIG Scalability</a>.
Présentation du SIG Scalability, avec l'ensemble des outils (par exemple <a href="https://github.com/kubernetes/perf-tests/tree/master/clusterloader2">ClusterLoader2</a>) qui permettent de valider les chiffres annoncés, du genre &quot;k8s v1.29 permet de gérer X pods sur Y nodes.</p>
</li>
</ul>
<p><img alt="SIG Scalability" class="zoomable" decoding="async" loading="lazy" src="/2024/03/2024-03-24-sigscalability.webp"></p>
<ul>
<li>
<p><a href="https://kccnceu2024.sched.com/event/1YeP7/leveling-up-wasm-support-in-kubernetes-matt-butcher-fermyon">Leveling up Wasm Support in Kubernetes</a>.
Présentation des produits Spin, et surtout SpinKube, qui permet de lancer des workloads WASM dans Kubernetes. Outil à suivre de près !</p>
</li>
<li>
<p><a href="https://kccnceu2024.sched.com/event/1YePm/confidential-containers-for-gpu-compute-incorporating-llms-in-a-lift-and-shift-strategy-for-ai-zvonko-kaiser-nvidia">Confidential Containers for GPU Compute: Incorporating LLMs in a Lift-and-Shift Strategy for AI</a>.
Comment partager le GPU lorsqu'un utilise des Confidentials Conteneurs : gestion du GPU Passthrough &amp; co. Pas simple !</p>
</li>
</ul>
<p><img alt="Confidential Containers" class="zoomable" decoding="async" loading="lazy" src="/2024/03/2024-03-24-confidentialcontainers.webp"></p>
<ul>
<li>
<p><a href="https://kccnceu2024.sched.com/event/1YhiE/enhancing-reliability-through-multi-cluster-deployment-leveraging-the-power-of-karmada-hongcai-ren-huawei-xiao-zhang-daocloud-ying-zhang-zendesk">Enhancing Reliability Through Multi-Cluster Deployment: Leveraging the Power of Karmada</a>. Présentation de l'outil <a href="https://karmada.io/">karmada</a>.</p>
</li>
<li>
<p>🌟 <a href="https://kccnceu2024.sched.com/event/1YhiK/cni-recap-and-update-casey-callendrello-isovalent-tomofumi-hayashi-red-hat">CNI: Recap and Update</a>.
Aperçu de la gestion du projet CNI, niveau organisation. Comment juste ajouter un verbe dans la définition d'une CNI peut être long et impacter beaucoup d'autres outils.</p>
</li>
</ul>
<p><img alt="CNI" class="zoomable" decoding="async" loading="lazy" src="/2024/03/2024-03-24-cni.webp"></p>
<ul>
<li>
<p><a href="https://kccnceu2024.sched.com/event/1YeSt/scaling-service-mesh-self-service-beyond-300-clusters-sumit-mathur-sushanth-kamath-a-intuit">Scaling Service Mesh: Self Service Beyond 300 Clusters</a>
Présentation de l'outil  <a href="https://github.com/intuit/naavik">naavik</a> pour gérer la synchro des Customs Resources Istio.</p>
</li>
<li>
<p><a href="https://kccnceu2024.sched.com/event/1YeSC/observable-feature-rollouts-with-opentelemetry-and-openfeature-daniel-dyla-michael-beemer-dynatrace">Observable Feature Rollouts with OpenTelemetry and OpenFeature</a>. Feature Flag + Métriques : beau combo pour faire des rollbacks quand ça se passe mal.</p>
</li>
</ul>
<p>J'aurais aimé assister à d'autres conférences comme celle sur les <a href="https://kccnceu2024.sched.com/event/1YePO/to-infinity-and-beyond-seamless-autoscaling-with-in-place-resource-resize-for-kubernetes-pods-aya-ozawa-cloudnatix-inc-kohei-ota-apple">Vertical Pod Autoscalers</a>, mais parfois les salles étaient pleines (&quot;<em>Room is full !!</em>), parfois j'étais pris par le temps à discuter sur les stands ou ailleurs.</p>
<p>Sur l'ensemble des confs, ce que j'ai le plus apprécié au final, c'est le retour des mainteneurs et toute l'organisation qui va avec les projets Open Source de grande ampleur. Autant faire du deep-dive sur un produit peut le faire chez soi, en lisant des docs et suivant des tutos youtube, autant comprendre le fonctionnement des <strong>SIG</strong> &amp; <strong>TAG</strong>, ça doit être vécu !</p>
<h2 id="derniers-mots">Derniers mots</h2>
<p>Cet article est assez long, j'espère que j'ai pu un peu retranscrire ces 3 jours de conférence à ma façon, et si vous arrivez à lire jusque-là je vous en félicite ;)</p>
<p>Ce fut une expérience très enrichissante, pas forcément sur la partie technique, mais beaucoup sur la partie organisation et communauté.</p>
<p>Le soir était propice aux échanges plus informels, avec beaucoup de discussions. Je retiens 2 remarques qui m'ont particulièrement touchées :</p>
<ol>
<li>
<p>Mes blog posts &amp; conférences sont appréciées, je vais donc essayer de sortir un peu plus d'articles, sans me mettre la pression non plus.</p>
</li>
<li>
<p>Certains aimeraient avoir la même motivation d'apprentissage de nouvelles technos quand ils auront mon âge. Je ne me pensais pas si vieux que ça, mais je le prends bien ;)</p>
</li>
</ol>
<p>Un grand merci à l'ensemble des personnes que j'ai pu croiser, et qui ont fait de mes quelques jours à Paris un agréable moment !</p>
<p>Prochain événement pour moi <a href="https://www.devoxx.fr/">DevoxxFR</a> 2024 !</p>
<p><img alt="Stands vides" class="zoomable" decoding="async" loading="lazy" src="/2024/03/2024-03-24-fin.webp"></p>
<h2 id="ils-en-parlent-aussi">Ils en parlent aussi</h2>
<p>Retrouvez deux autres articles de blog à ce sujet :</p>
<ul>
<li><a href="https://blog.zwindler.fr/2024/03/22/kubecon-eu-2024-vendredi/">Zwindler</a></li>
<li><a href="https://blog.cockpitio.com/events/kubecon-eu-paris-2024/">Cockpit.io</a></li>
<li><em>Demandez à rajouter le vôtre !</em></li>
</ul>]]></content:encoded></item><item><title>Kube-state-metrics et les Vertical Pod Autoscalers</title><link>https://www.vrchr.fr/posts/2024/01/09/kube-state-metrics-vpa/</link><pubDate>Tue, 09 Jan 2024 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2024/01/09/kube-state-metrics-vpa/</guid><description>Utilisez kube-state-metrics pour exploiter les métriques des VPAs</description><content:encoded><![CDATA[<p>Il y a quelque temps, je vous avais présenté <a href="/posts/2022/05/16/vpa-goldilocks/">Goldilocks</a> et les Vertical Pod Autoscalers en mode recommandation (VPAs) pour vous aider à dimensionner vos applications. Il était même possible assez facilement de récupérer les métriques des VPAs grâce à l'agent <a href="https://github.com/kubernetes/kube-state-metrics">kube-state-metrics</a>, en rajoutant le collecteur <code>verticalpodautoscalers</code> dans la configuration.</p>
<p>Malheureusement, depuis la version <a href="https://github.com/kubernetes/kube-state-metrics/blob/main/docs/customresourcestate-metrics.md#verticalpodautoscaler">2.9.0</a> de l'agent, la collecte de ces ressources a été supprimée.</p>
<p>Il existe cependant un moyen, moins simple, de récupérer ces métriques, qui sont dans les status des ressources VPAs. À l'occasion de la mise à jour de ma stack de monitoring, je vous explique dans cet article comment faire.</p>
<h2 id="kube-state-metrics-et-les-customresourcestatemetrics">Kube-state-metrics et les CustomResourceStateMetrics</h2>
<p>Comme dit plus haut, <a href="https://github.com/kubernetes/kube-state-metrics">kube-state-metrics</a> est un composant applicatif qui se déploie dans un cluster kubernetes, et permet d'exposer des métriques sur l'état (status) d'objets kubernetes. Il est installé par défaut lorsque vous déployez la <a href="https://github.com/prometheus-operator/kube-prometheus">stack prometheus</a>.</p>
<p>Vous retrouverez <a href="https://github.com/kubernetes/kube-state-metrics/tree/main/docs">ici</a> la liste des métriques qu'il permet d'exposer. On pourra par exemple avoir des informations sur le nombre de jobs créés, effectués, en erreur avec les métriques suivantes :</p>
<ul>
<li><code>kube_job_created</code></li>
<li><code>kube_job_complete</code></li>
<li><code>kube_job_failed</code></li>
</ul>
<p>L'application permet de générer beaucoup de métriques... Mais plus les VPAs ! Pas de panique, il est possible de s'en sortir avec une ressource particulière, les <a href="https://github.com/kubernetes/kube-state-metrics/blob/main/docs/customresourcestate-metrics.md">CustomResourceStateMetrics</a>.</p>
<h3 id="customresourcestatemetrics">CustomResourceStateMetrics</h3>
<p>Grâce aux <code>CustomResourceStateMetrics</code>, kube-state-metrics va aller chercher, dans les ressources spécifiées, les informations de status, et les exposer en tant que métriques.</p>
<p>Par exemple, prenons une ressource <code>NodePool</code> (<code>nodepools.kube.cloud.ovh.com</code>), qui permet de définir les nodepools et nombre de nœuds associés dans un cluster OVHcloud. Sur ces ressources sont indiquées dans le &quot;status&quot; le nombre de nœuds réellement disponibles dans le nodepool :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># kubectl get nodepools.kube.cloud.ovh.com general-worker-pool -o yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">kube.cloud.ovh.com/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">NodePool</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">general-worker-pool</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">desiredNodes</span><span class="p">:</span><span class="w"> </span><span class="m">4</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">flavor</span><span class="p">:</span><span class="w"> </span><span class="l">b2-15</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">maxNodes</span><span class="p">:</span><span class="w"> </span><span class="m">100</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">minNodes</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="nt">availableNodes</span><span class="p">:</span><span class="w"> </span><span class="m">4</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="nt">currentNodes</span><span class="p">:</span><span class="w"> </span><span class="m">4</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">  </span><span class="nt">upToDateNodes</span><span class="p">:</span><span class="w"> </span><span class="m">4</span><span class="w">
</span></span></span></code></pre></div><p>On peut alors définir une <code>CustomResourceStateMetrics</code> qui ira chercher ces informations (<code>availableNodes</code>, <code>currentNodes</code> &amp; <code>upToDateNodes</code>). Dans cette ressource, on va spécifier le type de ressource à lire (NodePool), où trouver les métriques (path), de quel type (gauge) :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">CustomResourceStateMetrics</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span>- <span class="nt">groupVersionKind</span><span class="p">:</span><span class="w"> </span><span class="c"># Quelle ressource aller chercher</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">        </span><span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">kube.cloud.ovh.com</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">        </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;NodePool&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">        </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;v1alpha1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">labelsFromPath</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">nodepool</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">metadata, name]</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">metrics</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;available_nodes_count&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">          </span><span class="nt">help</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Number of available nodes in the nodepool&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">          </span><span class="nt">each</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Gauge</span><span class="w"> </span><span class="c"># Quel type de métrique</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">            </span><span class="nt">gauge</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">status]</span><span class="w"> </span><span class="c"># Path où trouver l&#39;information</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">availableNodes] </span><span class="w"> </span><span class="c"># Valeur à récupérer</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;current_nodes_count&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span><span class="nt">help</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Number of current nodes in the nodepool&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">          </span><span class="nt">each</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Gauge</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">            </span><span class="nt">gauge</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">status]</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">currentNodes]</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;uptodate_nodes_count&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">          </span><span class="nt">help</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Number of up-to-date nodes in the nodepool&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">          </span><span class="nt">each</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Gauge</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">            </span><span class="nt">gauge</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">status]</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">upToDateNodes]</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">status, availableNodes]</span><span class="w"> </span><span class="c"># Path où trouver l&#39;information</span><span class="w">
</span></span></span></code></pre></div><p>Cela génèrera trois métriques simples :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">kube_customresource_available_nodes_count{customresource_group=&#34;kube.cloud.ovh.com&#34;,customresource_kind=&#34;NodePool&#34;,customresource_version=&#34;v1alpha1&#34;,nodepool=&#34;general-worker-pool&#34;} 4
</span></span><span class="line"><span class="ln">2</span><span class="cl">kube_customresource_current_nodes_count{customresource_group=&#34;kube.cloud.ovh.com&#34;,customresource_kind=&#34;NodePool&#34;,customresource_version=&#34;v1alpha1&#34;,nodepool=&#34;general-worker-pool&#34;} 4
</span></span><span class="line"><span class="ln">3</span><span class="cl">kube_customresource_uptodate_nodes_count{customresource_group=&#34;kube.cloud.ovh.com&#34;,customresource_kind=&#34;NodePool&#34;,customresource_version=&#34;v1alpha1&#34;,nodepool=&#34;general-worker-pool&#34;} 4
</span></span></code></pre></div><p>Sur des ressources un peu plus complexes, on peut itérer avec des <code>each</code>, récupérer des labels, etc. Je vous renvoie à la <a href="https://github.com/kubernetes/kube-state-metrics/blob/main/docs/customresourcestate-metrics.md">documentation</a> pour plus de détails.</p>
<h2 id="génération-de-métriques-pour-les-vpas">Génération de métriques pour les VPAs</h2>
<p>Si on revient à notre Vertical Pod Autoscaler, il va créer des ressources de type <code>VerticalPodAutoscaler</code>, dans lequel il stocke les informations relatives à ses recommandations (informations documentées <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/verticalpodautoscaler?hl=fr#verticalpodautoscalerspec_v1_autoscalingk8sio">ici</a>) :</p>
<ul>
<li><code>target</code> : Demande de ressources mémoire et de processeur recommandée pour le conteneur.</li>
<li><code>lowerBound</code> : Nombre minimum recommandé de demandes de ressources mémoire et de processeur pour le conteneur.</li>
<li><code>upperBound</code> : Nombre maximum recommandé de demandes de ressources mémoire et de processeur pour le conteneur.</li>
<li><code>uncappedTarget</code> : Dernière recommandation de ressources calculée par l'autoscaler, basée sur l'utilisation réelle des ressources, sans tenir compte de ContainerResourcePolicy.</li>
</ul>
<p>Côté ressource yaml, cela donne :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># kubectl get vpa goldilocks-example -o yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">autoscaling.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">VerticalPodAutoscaler</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">creator</span><span class="p">:</span><span class="w"> </span><span class="l">Fairwinds</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l">goldilocks</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">goldilocks-example</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">targetRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">example</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="nt">updatePolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">updateMode</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Off&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">  </span><span class="nt">conditions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">  </span>- <span class="nt">lastTransitionTime</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2023-08-21T08:16:27Z&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">status</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;True&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">RecommendationProvided</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">  </span><span class="nt">recommendation</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nt">containerRecommendations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">    </span>- <span class="nt">containerName</span><span class="p">:</span><span class="w"> </span><span class="l">container</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">      </span><span class="nt">lowerBound</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">        </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">15m</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">        </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1101864149&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">      </span><span class="nt">target</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">        </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">15m</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">        </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1238659775&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">      </span><span class="nt">uncappedTarget</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">        </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">15m</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">        </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1238659775&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">      </span><span class="nt">upperBound</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">        </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">25m</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">        </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1381172105&#34;</span><span class="w">
</span></span></span></code></pre></div><p>Les informations sont donc la, recommandations CPU &amp; RAM, on peut créer notre <code>CustomResourceStateMetrics</code> !</p>
<h3 id="installation-pour-kube-prometheus-stack">Installation pour kube-prometheus-stack</h3>
<p>Je l'ai mentionné en début d'article, j'utilise la stack prometheus, installée avec le chart Helm <a href="https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack">kube-prometheus-stack</a>. Il me faut alors rajouter l'ensemble des <code>CustomResourceStateMetrics</code> que je souhaite récupérer dans mon fichier <code>values.yaml</code>. Il faut également autoriser kube-state-metrics à aller scraper ces ressources. Ce qui nous donne (extrait) :</p>
<ul>
<li>
<p>Pour le RBAC :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># extract du fichier values.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nt">kube-state-metrics</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">rbac</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">extraRules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">      </span>- <span class="nt">apiGroups</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;autoscaling.k8s.io&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">        </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;verticalpodautoscalers&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">        </span><span class="nt">verbs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;list&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;watch&#34;</span><span class="p">]</span><span class="w">
</span></span></span></code></pre></div></li>
<li>
<p>Pour les <code>CustomResourceStateMetrics</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># extract du fichier values.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kube-state-metrics</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">customResourceState</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">CustomResourceStateMetrics</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">          </span>- <span class="nt">groupVersionKind</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">              </span><span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">autoscaling.k8s.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">              </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;VerticalPodAutoscaler&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">              </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;v1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">            </span><span class="nt">labelsFromPath</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">              </span><span class="nt">verticalpodautoscaler</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">metadata, name]</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">              </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">metadata, namespace]</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">              </span><span class="nt">target_api_version</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">spec, targetRef, apiVersion]</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">              </span><span class="nt">target_kind</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">spec, targetRef, kind]</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">              </span><span class="nt">target_name</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">spec, targetRef, name]</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">            </span><span class="nt">metrics</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">              </span><span class="c"># Labels</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">              </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;verticalpodautoscaler_labels&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">                </span><span class="nt">help</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;VPA container recommendations. Kubernetes labels converted to Prometheus labels&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">                </span><span class="nt">each</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">                  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Info</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">                  </span><span class="nt">info</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">                    </span><span class="nt">labelsFromPath</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">                      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">metadata, name]</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">              </span><span class="c"># Memory Information</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">              </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;verticalpodautoscaler_status_recommendation_containerrecommendations_target&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">                </span><span class="nt">help</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;VPA container recommendations for memory. Target resources the VerticalPodAutoscaler recommends for the container.&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">                </span><span class="nt">each</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">                  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Gauge</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">                  </span><span class="nt">gauge</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">                    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">status, recommendation, containerRecommendations]</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">                    </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">target, memory]</span><span class="w">
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="w">                    </span><span class="nt">labelsFromPath</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="w">                      </span><span class="nt">container</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">containerName]</span><span class="w">
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="w">                </span><span class="nt">commonLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="w">                  </span><span class="nt">resource</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;memory&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="w">                  </span><span class="nt">unit</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;byte&#34;</span><span class="w">
</span></span></span></code></pre></div></li>
</ul>
<p>Le fichier étant particulièrement long, vous retrouverez le paramétrage complet dans ce <a href="https://gist.github.com/rverchere/780bd988b8f385a00644a1ec9f425be3">gist</a>.</p>
<p>Il ne reste plus qu'à mettre à jour notre chart helm, et on aura les métriques que nous voulons. À date, je suis sur le chart 55.6.0, avec prometheus en version v0.70.0 :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl">$ helm upgrade prometheus prometheus-community/kube-prometheus-stack -f custom-values.yaml
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">Release <span class="s2">&#34;prometheus&#34;</span> has been upgraded. Happy Helming!
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">NAME: prometheus
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">LAST DEPLOYED: Wed Jan <span class="m">10</span> 09:26:19 <span class="m">2024</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">NAMESPACE: observability
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">STATUS: deployed
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">REVISION: <span class="m">30</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">NOTES:
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">kube-prometheus-stack has been installed. Check its status by running:
</span></span><span class="line"><span class="ln">10</span><span class="cl">  kubectl --namespace observability get pods -l <span class="s2">&#34;release=prometheus&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">Visit https://github.com/prometheus-operator/kube-prometheus <span class="k">for</span> instructions on how to create <span class="p">&amp;</span> configure Alertmanager and Prometheus instances using the Operator.
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">$ helm list
</span></span><span class="line"><span class="ln">15</span><span class="cl">NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART                           APP VERSION
</span></span><span class="line"><span class="ln">16</span><span class="cl">prometheus      observability   <span class="m">30</span>              2024-01-10 09:26:19.09370522 +0100 CET  deployed        kube-prometheus-stack-55.6.0    v0.70.0
</span></span></code></pre></div><p>On vérifie au démarrage du pod <code>kube-state-metrics</code>, à priori la configuration est prise en compte :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">$ kubectl stern prom-kube-state-metrics-66dcd8b56b-snvz4
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">+ prom-kube-state-metrics-66dcd8b56b-snvz4 › kube-state-metrics
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">[...]
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">prom-kube-state-metrics-66dcd8b56b-snvz4 kube-state-metrics I0108 15:06:07.348917       1 wrapper.go:120] &#34;Starting kube-state-metrics&#34;
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">prom-kube-state-metrics-66dcd8b56b-snvz4 kube-state-metrics I0108 15:06:10.401886       1 registry_factory.go:76] &#34;Info metric does not have _info suffix&#34; gvk=&#34;autoscaling.k8s.io_v1_VerticalPodAutoscaler&#34; name=&#34;verticalpodautoscaler_labels&#34;
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">prom-kube-state-metrics-66dcd8b56b-snvz4 kube-state-metrics I0108 15:06:10.402053       1 config.go:82] &#34;Using custom resource plural&#34; resource=&#34;autoscaling.k8s.io_v1_VerticalPodAutoscaler&#34; plural=&#34;verticalpodautoscalers&#34;
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">[...]
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">prom-kube-state-metrics-66dcd8b56b-snvz4 kube-state-metrics I0108 15:06:10.403047       1 custom_resource_metrics.go:79] &#34;Custom resource state added metrics&#34; familyNames=[&#34;kube_customresource_verticalpodautoscaler_labels&#34;,&#34;kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_target&#34;,&#34;kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_lowerbound&#34;,&#34;kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_upperbound&#34;,&#34;kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_uncappedtarget&#34;,&#34;kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_target&#34;,&#34;kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_lowerbound&#34;,&#34;kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_upperbound&#34;,&#34;kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_uncappedtarget&#34;]
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">prom-kube-state-metrics-66dcd8b56b-snvz4 kube-state-metrics I0108 15:06:10.403134       1 builder.go:271] &#34;Active resources&#34; activeStoreNames=&#34;certificatesigningrequests,configmaps,cronjobs,daemonsets,deployments,endpoints,horizontalpodautoscalers,ingresses,jobs,leases,limitranges,mutatingwebhookconfigurations,namespaces,networkpolicies,nodes,persistentvolumeclaims,persistentvolumes,poddisruptionbudgets,pods,replicasets,replicationcontrollers,resourcequotas,secrets,services,statefulsets,storageclasses,validatingwebhookconfigurations,volumeattachments,autoscaling.k8s.io/v1, Resource=verticalpodautoscalers&#34;
</span></span><span class="line"><span class="ln">10</span><span class="cl">prom-kube-state-metrics-66dcd8b56b-snvz4 kube-state-metrics E0109 08:26:07.928093       1 registry_factory.go:682] &#34;kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_target&#34; err=&#34;[status,recommendation,containerRecommendations]: got nil while resolving path&#34;
</span></span></code></pre></div><p>Nous avons une erreur sur un path qui n'est pas trouvé, je n'ai pour l'instant pas cherché d'où venait cette erreur. Si vous lisez ces lignes et avez la solution, je suis preneur ;)</p>
<h3 id="nom-des-métriques">Nom des métriques</h3>
<p>Avec l'utilisation des <code>CustomResourceStateMetrics</code>, on ne peut pas choisir le nom complet des métriques, elles seront toujours préfixées par <code>kube_customresource</code>.</p>
<p>Pour être le plus proche possible des anciennes métriques proposées, j'ai donc fait le choix de nommer les métriques ainsi :</p>
<ul>
<li><code>kube_verticalpodautoscaler_labels</code> -&gt; <code>kube_customresource_verticalpodautoscaler_labels</code></li>
<li><code>kube_verticalpodautoscaler_status_recommendation_containerrecommendations_target</code> -&gt; <code>kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_target</code></li>
<li><code>kube_verticalpodautoscaler_status_recommendation_containerrecommendations_lowerbound</code> -&gt; <code>kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_lowerbound</code></li>
<li><code>kube_verticalpodautoscaler_status_recommendation_containerrecommendations_upperbound</code> -&gt; <code>kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_upperbound</code></li>
<li><code>kube_verticalpodautoscaler_status_recommendation_containerrecommendations_uncappedtarget</code> -&gt; <code>kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_uncappedtarget</code></li>
</ul>
<h2 id="résultat">Résultat</h2>
<p>Si l'on interroge <code>kube-state-metrics</code> directement, on peut voir les métriques :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl port-forward svc/prom-kube-state-metrics 8080:8080
</span></span><span class="line"><span class="ln">2</span><span class="cl"> 
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ curl localhost:8080/metrics
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="o">[</span>...<span class="o">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># HELP kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_target VPA container recommendations for memory. Target resources the VerticalPodAutoscaler recommends for the container.</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># TYPE kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_target gauge</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_target<span class="o">{</span><span class="nv">container</span><span class="o">=</span><span class="s2">&#34;storegateway&#34;</span>,customresource_group<span class="o">=</span><span class="s2">&#34;autoscaling.k8s.io&#34;</span>,customresource_kind<span class="o">=</span><span class="s2">&#34;VerticalPodAutoscaler&#34;</span>,customresource_version<span class="o">=</span><span class="s2">&#34;v1&#34;</span>,namespace<span class="o">=</span><span class="s2">&#34;observability&#34;</span>,resource<span class="o">=</span><span class="s2">&#34;memory&#34;</span>,target_api_version<span class="o">=</span><span class="s2">&#34;apps/v1&#34;</span>,target_kind<span class="o">=</span><span class="s2">&#34;StatefulSet&#34;</span>,target_name<span class="o">=</span><span class="s2">&#34;thanos-storegateway&#34;</span>,unit<span class="o">=</span><span class="s2">&#34;byte&#34;</span>,verticalpodautoscaler<span class="o">=</span><span class="s2">&#34;goldilocks-thanos-storegateway&#34;</span><span class="o">}</span> 1.048576e+08
</span></span><span class="line"><span class="ln">8</span><span class="cl">kube_customresource_verticalpodautoscaler_status_recommendation_containerrecommendations_target<span class="o">{</span><span class="nv">container</span><span class="o">=</span><span class="s2">&#34;node-exporter&#34;</span>,customresource_group<span class="o">=</span><span class="s2">&#34;autoscaling.k8s.io&#34;</span>,customresource_kind<span class="o">=</span><span class="s2">&#34;VerticalPodAutoscaler&#34;</span>,customresource_version<span class="o">=</span><span class="s2">&#34;v1&#34;</span>,namespace<span class="o">=</span><span class="s2">&#34;observability&#34;</span>,resource<span class="o">=</span><span class="s2">&#34;memory&#34;</span>,target_api_version<span class="o">=</span><span class="s2">&#34;apps/v1&#34;</span>,target_kind<span class="o">=</span><span class="s2">&#34;DaemonSet&#34;</span>,target_name<span class="o">=</span><span class="s2">&#34;prom-prometheus-node-exporter&#34;</span>,unit<span class="o">=</span><span class="s2">&#34;byte&#34;</span>,verticalpodautoscaler<span class="o">=</span><span class="s2">&#34;goldilocks-prom-prometheus-node-exporter&#34;</span><span class="o">}</span> 1.048576e+08
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="o">[</span>...<span class="o">]</span>
</span></span></code></pre></div><p>On se connecte sur Prometheus (ou même mieux, Grafana), et on a maintenant nos métriques, nickel !</p>
<p><img alt="Prometheus Query" class="zoomable" decoding="async" loading="lazy" src="/2024/01/2024-01-09-vpa-customresourcestatemetrics.webp"></p>
<p>Et bonus, le tableau de bord Grafana utilisant ces métriques a été mis à jour, vous le retrouverez ici : <a href="https://grafana.com/grafana/dashboards/16294-vpa-recommendations/">https://grafana.com/grafana/dashboards/16294-vpa-recommendations/</a></p>
<h3 id="ressources">Ressources</h3>
<p>Quelques liens m'ayant permis de mettre à jour les métriques :</p>
<ul>
<li><a href="https://github.com/kubernetes/kube-state-metrics/blob/main/docs/customresourcestate-metrics.md#verticalpodautoscaler">https://github.com/kubernetes/kube-state-metrics/blob/main/docs/customresourcestate-metrics.md#verticalpodautoscaler</a></li>
<li><a href="https://github.com/kubernetes/kube-state-metrics/issues/2041#issuecomment-1614327806">https://github.com/kubernetes/kube-state-metrics/issues/2041#issuecomment-1614327806</a></li>
</ul>]]></content:encoded></item><item><title>Minimisez l'effet démo avec demo-magic et asciinema !</title><link>https://www.vrchr.fr/posts/2023/12/18/demo-magic/</link><pubDate>Mon, 18 Dec 2023 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2023/12/18/demo-magic/</guid><description>Minimisez l&amp;#39;effet démo avec les outils demo-magic et asciinema !</description><content:encoded><![CDATA[<p>Super, j'ai été sélectionné pour présenter un sujet à l'<a href="https://www.opensource-experience.com/">Open Source Experience 2023</a> ! Je connais mon sujet, l'ai déjà présenté au meetup DevOps Aix-Marseille, ça devrait aller... Ah, mais la présentation ne dure que 20 minutes, et je vois dans la FAQ des speakers qu'il n'y a pas de micro cravates, et donc potentiellement pas les 2 mains libres pour faire les 2 démos, ça va être sport!</p>
<p>Un coup de stress, je dois drastiquement réduire le temps de ma présentation. Pour la partie théorique ça peut aller, je vais zapper des choses pas forcément essentielles, mais j'ai toujours mes démos à faire, et ça je ne peux pas faire l'impasse, mon sujet c'est justement la démo de 2 outils, et le moindre grain de sable peut faire exploser le timing déjà très court !</p>
<p>Je me rappelle alors du <a href="/posts/2022/04/21/devoxxfr-jeudi/">Devoxx France 2022</a> où <a href="https://twitter.com/boombaprealm">Antoine Ceol</a> avait utilisé une application pour sa démo sur <a href="https://youtu.be/QCsFFAc5ffc">GIT</a>. Pour une fois que mon blog sert :P</p>
<p>Je vais alors vous présenter 2 outils pour accélérer vos démos, et minimiser les erreurs pendant vos présentations.</p>
<h2 id="demo-magic">Demo-magic</h2>
<p><a href="https://github.com/paxtonhare/demo-magic">Demo-magic</a> est un script shell assez simple qui vous permet d'automatiser la frappe au clavier de commandes, un peu comme du playback. Le concept est simple, on écrit les commandes à l'avance, et lorsque vient le temps de la démo, on appelle <code>demo-magic</code> qui fera le travail presque à votre place.</p>
<h3 id="installation">Installation</h3>
<p>En pré-requis, il faut installer la commande <code>pv</code> pour simuler la tape au clavier (ici installation sous Debian). Ensuite, il vous faut récupérer le <a href="https://raw.githubusercontent.com/paxtonhare/demo-magic/master/demo-magic.sh">script</a> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ sudo apt install pv
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ curl -LO https://raw.githubusercontent.com/paxtonhare/demo-magic/master/demo-magic.sh
</span></span></code></pre></div><p>Puis, il suffit de &quot;sourcer&quot; ce script dans votre propre script / prompt bash, qui contiendra les commandes à exécuter / simuler :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">########################</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># include the magic</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># https://github.com/paxtonhare/demo-magic</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">########################</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">. demo-magic.sh
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl">p <span class="s2">&#34;apt install cowsay&#34;</span>
</span></span></code></pre></div><h3 id="utilisation">Utilisation</h3>
<p>Une fois l'outil inclus, vous pouvez écrire les différentes étapes de votre démo. Le script propose plusieurs fonctions :</p>
<ul>
<li><code>p</code> : Affiche simplement la commande, sans l'exécuter. Il faudra appuyer sur <code>Entrée</code> pour l'affichage</li>
<li><code>pe</code> : Affiche et exécute la commande. Il faudra appuyer sur <code>Entrée</code> pour l'exécution</li>
<li><code>pei</code> : Affiche et exécute la commande immédiatement.</li>
</ul>
<p>Vu la complexité du script, vous pouvez l'étendre sans trop de problèmes ;)</p>
<p>D'ailleurs, j'ai rajouté de mon côté <code>pi</code>, je vous laisse deviner sa fonction.</p>
<p>À l'appel du script, il est possible également de spécifier 2-3 paramètres comme le fait d'attendre entre les actions ou pas.</p>
<p>Voici ci-après un exemple d'utilisation :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">p <span class="s2">&#34;# Clone, Python venv and install requirements&#34;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">pi <span class="s2">&#34;python3 -m venv krr-env&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">pei <span class="s2">&#34;source krr-env/bin/activate&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># Clone &amp; Install Krr</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">pi <span class="s2">&#34;git clone https://github.com/robusta-dev/krr&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">pei <span class="s2">&#34;cd krr&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">pi <span class="s2">&#34;pip install -r requirements.txt&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># Use Krr</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">pe <span class="s2">&#34;python krr.py simple -n kube-system&#34;</span>
</span></span></code></pre></div><h3 id="paramétrage">Paramétrage</h3>
<p>Il est possible de &quot;customiser&quot; le script via quelques variables d'environnement, comme la vitesse d'écriture, la couleur du prompt, etc. Il faut simplement surcharger les variables :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Set demo-magic options</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nv">TYPE_SPEED</span><span class="o">=</span><span class="m">50</span> <span class="c1"># Accelerate typing</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nv">DEMO_CMD_COLOR</span><span class="o">=</span><span class="s2">&#34;&#34;</span> <span class="c1"># No bold</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nv">DEMO_PROMPT</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">PURPLE</span><span class="si">}</span>$<span class="s2"> </span><span class="si">${</span><span class="nv">COLOR_RESET</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nv">DEMO_COMMENT_COLOR</span><span class="o">=</span><span class="nv">$CYAN</span>
</span></span></code></pre></div><p>Et voilà, il n'y a plus qu'à lancer votre démo, et par magie tout s'exécute (presque) tout seul ! Voici le résultat de cette <a href="https://gitlab.com/opsrel.io/requests-limits-demo/-/blob/d3286c8260936dccab6103eb2d9d1f99b8d9448c/requests-limits-demo-krr.sh">démo</a> :</p>
<div id='krr-demo'></div>
<script src="https://www.vrchr.fr//js/asciinema-player.min.js"></script>
<script>
    AsciinemaPlayer.create('/casts/krr-demo.cast', document.getElementById('krr-demo'), {
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    });
</script>
<p>Super, j'ai &quot;automatisé&quot; ma démo, mais je ne suis pas serein à 100%... Si jamais j'ai un problème de réseau, que ma plateforme est en vrac, je peux scripter autant que je veux mes actions, ça ne marchera pas ! Vient alors le deuxième outil à la rescousse : asciinema !</p>
<h2 id="asciinema">Asciinema</h2>
<p><a href="https://asciinema.org/">Asciinema</a> est un logiciel super pratique, qui va enregistrer ce qui se passe dans votre terminal. Vous pourrez ensuite sauvegarder et rejouer l'enregistrement en local, ou bien le télécharger et le partager en ligne sur le site <a href="https://asciinema.org/">https://asciinema.org/</a>.</p>
<h3 id="installation-1">Installation</h3>
<p>Toujours sous Debian, un coup d'apt et c'est réglé. On peut ensuite s'authentifier pour pouvoir télécharger le résultat pour partage.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ sudo apt install asciinema
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ asciinema auth
</span></span><span class="line"><span class="ln">3</span><span class="cl">Open the following URL in a web browser to link your install ID with your asciinema.org user account:
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">https://asciinema.org/connect/xxxx-xx-xxx-xxxx
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl">This will associate all recordings uploaded from this machine <span class="o">(</span>past and future ones<span class="o">)</span> to your account, and allow you to manage them <span class="o">(</span>change title/theme, delete<span class="o">)</span> at asciinema.org.
</span></span></code></pre></div><h3 id="utilisation-1">Utilisation</h3>
<p>Plusieurs arguments possibles :</p>
<ul>
<li><code>rec</code> : enregistre la session du terminal</li>
<li><code>play</code> : joue une session enregistrée précédemment</li>
<li><code>cat</code> : affiche le contenu du fichier enregistré</li>
<li><code>upload</code> : télécharge sur le site la session enregistrée</li>
</ul>
<p>Dans notre cas, on va enregistrer l'exécution du script (ici <code>bonjour.sh</code>)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="ln">1</span><span class="cl"><span class="err">❯</span> <span class="n">asciinema</span> <span class="n">rec</span> <span class="o">-</span><span class="n">c</span> <span class="o">./</span><span class="n">bonjour</span><span class="o">.</span><span class="n">sh</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">asciinema</span><span class="p">:</span> <span class="n">recording</span> <span class="n">asciicast</span> <span class="n">to</span> <span class="o">/</span><span class="n">tmp</span><span class="o">/</span><span class="n">tmp00kyltyr</span><span class="o">-</span><span class="n">ascii</span><span class="o">.</span><span class="n">cast</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">asciinema</span><span class="p">:</span> <span class="n">exit</span> <span class="n">opened</span> <span class="n">program</span> <span class="n">when</span> <span class="n">you</span><span class="s1">&#39;re done</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">Bonjour</span> <span class="o">!</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">asciinema</span><span class="p">:</span> <span class="n">recording</span> <span class="n">finished</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">asciinema</span><span class="p">:</span> <span class="n">press</span> <span class="o">&lt;</span><span class="n">enter</span><span class="o">&gt;</span> <span class="n">to</span> <span class="n">upload</span> <span class="n">to</span> <span class="n">asciinema</span><span class="o">.</span><span class="n">org</span><span class="p">,</span> <span class="o">&lt;</span><span class="n">ctrl</span><span class="o">-</span><span class="n">c</span><span class="o">&gt;</span> <span class="n">to</span> <span class="n">save</span> <span class="n">locally</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">asciinema</span><span class="p">:</span> <span class="n">asciicast</span> <span class="n">saved</span> <span class="n">to</span> <span class="o">/</span><span class="n">tmp</span><span class="o">/</span><span class="n">tmp00kyltyr</span><span class="o">-</span><span class="n">ascii</span><span class="o">.</span><span class="n">cast</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="o">~</span>
</span></span></code></pre></div><p>Vous pouvez ensuite rejouer la commande avec <code>asciinema play</code>.</p>
<p>On retrouvera alors des exemples de ma démo super rapide ici (je ne sais pas jusqu'à quand elles seront en ligne, les enregistrements ne restant pas indéfiniment sur le site) :</p>
<ul>
<li><a href="https://asciinema.org/a/DJPEsCuSf5yNJnyZfQHnyb0gI">Krr demo</a></li>
<li><a href="https://asciinema.org/a/GKNmnzPTjpvjoCbc6UlAQnnx5">Goldilocks demo</a></li>
</ul>
<p>Vous aurez remarqué que la vidéo du paragraphe précédent est de l'asciinema, non ?</p>
<h2 id="conclusion">Conclusion</h2>
<p>Et voilà ! On a maintenant de quoi jouer notre démo sans accrocs :</p>
<ul>
<li>Une démo automatisée avec <code>demo-magic</code></li>
<li>Une démo enregistrée avec <code>asciinema</code></li>
</ul>
<p>Et pour l'anecdote, mes démos se sont très bien passées, dans les temps ! Et je ne suis pas le seul à avoir du faire un travail de synthèse pour l'OXSP, 20 minutes c'est vraiment court ;)</p>]]></content:encoded></item><item><title>Kubestr et CNPG Bench pour tester vos volumes persistants</title><link>https://www.vrchr.fr/posts/2023/10/25/kubestr-cnpg-bench/</link><pubDate>Wed, 25 Oct 2023 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2023/10/25/kubestr-cnpg-bench/</guid><description>Testez la performance de vos Volumes Persistants avec 2 outils; kubestr et cnpg-bench</description><content:encoded><![CDATA[<p>&quot;C'est lent&quot;, &quot;Ça marche mieux sur mon poste local&quot;, des remarques que j'ai eues il y a quelque temps sur des bases de données hébergées sur Kubernetes, avec des Volumes Persistants. Souhaitant avoir des données objectives avant de râler contre ceux qui se plaignent, j'ai trouvé 2 outils sympa pour tester les performances des PV (Persistant Volumes) sur mes clusters. Voici un aperçu.</p>
<p><strong>Note</strong> : Je ne vais pas expliquer ici comment tuner vos disques, ni tuner les applications de test pour optimiser les perfs.</p>
<h2 id="kubestr">Kubestr</h2>
<p><a href="https://kubestr.io/">Kubestr</a> est une collection d'outils pour &quot;découvrir, valider et évaluer les options de stockages sur Kubernetes&quot;. C'est un outil fournit par <a href="https://www.kasten.io/">Kasten</a>, un des leaders des solutions de protection des données en environnement Kubernetes.</p>
<p>Parmi les outils proposés par <code>kubestr</code>, on retrouve le fameux <a href="https://fio.readthedocs.io/en/latest/fio_doc.html">fio</a>, que les &quot;vieux&quot; sysadmin comme moi connaissent. Pour faire simple, <code>fio</code> va faire des tests de lecture / écriture avec plusieurs paramètres, et en sortir des stats.</p>
<p>Ainsi, <code>kubestr</code> va nous permettre assez simplement de :</p>
<ul>
<li>Créer un volume de taille définie à partir d'une classe de stockage choisie</li>
<li>Lancer un test de performances avec FIO</li>
<li>Nous donner les résultats.</li>
</ul>
<p>Un schéma repris du <a href="https://www.kasten.io/kubernetes/resources/blog/benchmarking-kubernetes-storage-with-kubestr">blogpost</a> de Kasten expliquant le process :</p>
<p><img alt="Benchmarking and Evaluating Your Kubernetes Storage with Kubestr" class="zoomable" decoding="async" loading="lazy" src="https://www.kasten.io/hs-fs/hubfs/Blog%20Images/Kubestr/evaluate-kubestr.png?width=1600&name=evaluate-kubestr.png"></p>
<h3 id="installation">Installation</h3>
<p>Kubestr est un binaire qui se télécharge directement depuis la page de releases <a href="https://github.com/kastenhq/kubestr">GitHub</a>. À la date d'écriture de l'article, nous sommes à la version v0.4.41. On récupère l'archive, on la décompresse et on lui donne les droits d'exécution :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="ln">1</span><span class="cl"><span class="o">$</span> <span class="n">curl</span> <span class="o">-</span><span class="n">LJO</span> <span class="n">https</span><span class="p">:</span><span class="o">//</span><span class="n">github</span><span class="o">.</span><span class="n">com</span><span class="o">/</span><span class="n">kastenhq</span><span class="o">/</span><span class="n">kubestr</span><span class="o">/</span><span class="n">releases</span><span class="o">/</span><span class="n">download</span><span class="o">/</span><span class="n">v0</span><span class="o">.</span><span class="mf">4.41</span><span class="o">/</span><span class="n">kubestr_0</span><span class="o">.</span><span class="mf">4.41</span><span class="n">_Linux_amd64</span><span class="o">.</span><span class="n">tar</span><span class="o">.</span><span class="n">gz</span> 
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">$</span> <span class="n">tar</span> <span class="n">zxvf</span> <span class="n">kubestr_0</span><span class="o">.</span><span class="mf">4.41</span><span class="n">_Linux_amd64</span><span class="o">.</span><span class="n">tar</span><span class="o">.</span><span class="n">gz</span> <span class="n">kubestr</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="o">$</span> <span class="n">chmod</span> <span class="o">+</span><span class="n">x</span> <span class="n">kubestr</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="o">$</span> <span class="o">./</span><span class="n">kubestr</span> <span class="o">--</span><span class="n">help</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">kubestr</span> <span class="n">is</span> <span class="n">a</span> <span class="k">tool</span> <span class="n">that</span> <span class="n">will</span> <span class="n">scan</span> <span class="n">your</span> <span class="n">k8s</span> <span class="n">cluster</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">                <span class="ow">and</span> <span class="n">validate</span> <span class="n">that</span> <span class="n">the</span> <span class="n">storage</span> <span class="n">systems</span> <span class="ow">in</span> <span class="n">place</span> <span class="n">as</span> <span class="n">well</span> <span class="n">as</span> <span class="n">run</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">                <span class="n">performance</span> <span class="n">tests</span><span class="o">.</span>
</span></span></code></pre></div><h3 id="utilisation">Utilisation</h3>
<p>Il existe plusieurs options d'utilisation de l'outil, ce qui nous intéresse ici est le module &quot;FIO&quot;. Plusieurs options de lancement sont possibles, notamment :</p>
<ul>
<li><code>-f, --fiofile</code> : un fichier de configuration fio. On pourra trouver des exemples <a href="https://github.com/axboe/fio/tree/master/examples">ici</a></li>
<li><code>-z, --size</code> : taille du volume qui sera créé. Selon les providers, les I/O dépendent de la taille du volume (exemple avec le <a href="https://www.ovhcloud.com/fr/public-cloud/block-storage/">High Speed Gen2</a> de chez OVHCloud)</li>
<li><code>-s, --storageclass</code> : type de StorageClass du volume persistant utilisé pour le test</li>
</ul>
<p>Pour l'exemple, on va tester les 2 types de classe de stockage par défaut d'un cluster OVHCloud, avec un volume de 25Gb.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">$ ./kubestr fio -z 25Gi -s csi-cinder-high-speed
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">PVC created kubestr-fio-pvc-wn4f6
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">Pod created kubestr-fio-pod-hxfvs
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">Running FIO test (default-fio) on StorageClass (csi-cinder-high-speed) with a PVC of Size (25Gi)
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">Elapsed time- 1m20.178437269s
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">FIO test results:
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">FIO version - fio-3.34
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">Global options - ioengine=libaio verify=0 direct=1 gtod_reduce=1
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">JobName: read_iops
</span></span><span class="line"><span class="ln">12</span><span class="cl">  blocksize=4K filesize=2G iodepth=64 rw=randread
</span></span><span class="line"><span class="ln">13</span><span class="cl">read:
</span></span><span class="line"><span class="ln">14</span><span class="cl">  IOPS=1412.357544 BW(KiB/s)=5666
</span></span><span class="line"><span class="ln">15</span><span class="cl">  iops: min=1220 max=1724 avg=1427.666626
</span></span><span class="line"><span class="ln">16</span><span class="cl">  bw(KiB/s): min=4880 max=6896 avg=5711.066895
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">JobName: write_iops
</span></span><span class="line"><span class="ln">19</span><span class="cl">  blocksize=4K filesize=2G iodepth=64 rw=randwrite
</span></span><span class="line"><span class="ln">20</span><span class="cl">write:
</span></span><span class="line"><span class="ln">21</span><span class="cl">  IOPS=825.023193 BW(KiB/s)=3316
</span></span><span class="line"><span class="ln">22</span><span class="cl">  iops: min=462 max=1292 avg=827.566650
</span></span><span class="line"><span class="ln">23</span><span class="cl">  bw(KiB/s): min=1848 max=5170 avg=3310.566650
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl">JobName: read_bw
</span></span><span class="line"><span class="ln">26</span><span class="cl">  blocksize=128K filesize=2G iodepth=64 rw=randread
</span></span><span class="line"><span class="ln">27</span><span class="cl">read:
</span></span><span class="line"><span class="ln">28</span><span class="cl">  IOPS=1657.303345 BW(KiB/s)=212667
</span></span><span class="line"><span class="ln">29</span><span class="cl">  iops: min=1490 max=1858 avg=1671.933350
</span></span><span class="line"><span class="ln">30</span><span class="cl">  bw(KiB/s): min=190720 max=237824 avg=214017.406250
</span></span><span class="line"><span class="ln">31</span><span class="cl">
</span></span><span class="line"><span class="ln">32</span><span class="cl">JobName: write_bw
</span></span><span class="line"><span class="ln">33</span><span class="cl">  blocksize=128k filesize=2G iodepth=64 rw=randwrite
</span></span><span class="line"><span class="ln">34</span><span class="cl">write:
</span></span><span class="line"><span class="ln">35</span><span class="cl">  IOPS=862.746399 BW(KiB/s)=110966
</span></span><span class="line"><span class="ln">36</span><span class="cl">  iops: min=692 max=1298 avg=865.000000
</span></span><span class="line"><span class="ln">37</span><span class="cl">  bw(KiB/s): min=88576 max=166144 avg=110755.734375
</span></span><span class="line"><span class="ln">38</span><span class="cl">
</span></span><span class="line"><span class="ln">39</span><span class="cl">Disk stats (read/write):
</span></span><span class="line"><span class="ln">40</span><span class="cl">  sdb: ios=51692/28087 merge=445/670 ticks=2147425/2151581 in_queue=4303451, util=99.532051%
</span></span><span class="line"><span class="ln">41</span><span class="cl">  -  OK
</span></span></code></pre></div><p>La même commande sur une StorageClass &quot;classic&quot; :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">$ ./kubestr fio -z 25Gi -s csi-cinder-classic
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">PVC created kubestr-fio-pvc-8pnmw
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">Pod created kubestr-fio-pod-z9bnn
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">Running FIO test (default-fio) on StorageClass (csi-cinder-classic) with a PVC of Size (25Gi)
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">Elapsed time- 41.494978608s
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">FIO test results:
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">FIO version - fio-3.34
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">Global options - ioengine=libaio verify=0 direct=1 gtod_reduce=1
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">JobName: read_iops
</span></span><span class="line"><span class="ln">12</span><span class="cl">  blocksize=4K filesize=2G iodepth=64 rw=randread
</span></span><span class="line"><span class="ln">13</span><span class="cl">read:
</span></span><span class="line"><span class="ln">14</span><span class="cl">  IOPS=120.751854 BW(KiB/s)=498
</span></span><span class="line"><span class="ln">15</span><span class="cl">  iops: min=44 max=184 avg=123.064514
</span></span><span class="line"><span class="ln">16</span><span class="cl">  bw(KiB/s): min=176 max=736 avg=492.354828
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">[...]
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">Disk stats (read/write):
</span></span><span class="line"><span class="ln">21</span><span class="cl">  sdb: ios=4468/2655 merge=8/151 ticks=2224172/1996479 in_queue=4222117, util=99.475037%
</span></span><span class="line"><span class="ln">22</span><span class="cl">  -  OK
</span></span></code></pre></div><p>L'application a créé à chaque fois un PVC avec la bonne taille, dans la bonne StorageClass, créé un pod, monté le volume persistant, fait les tests FIO et nous a sorti les résultats. Easy !</p>
<h3 id="résultats">Résultats</h3>
<p>Les résultats nous arrivent par défaut sur la console, et on peut voir que plusieurs tests ont été faits :</p>
<ul>
<li>read_iops</li>
<li>write_iops</li>
<li>read_bw</li>
<li>write_bw</li>
</ul>
<p>Je ne vais pas commenter les performances des volumes persistants ici, mais on voit bien que le &quot;high-speed&quot; est plus rapide que le &quot;classic&quot; ;)</p>
<p>Exemple sur les tests en lecture :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl"># High-Speed
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">JobName: read_iops
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  blocksize=4K filesize=2G iodepth=64 rw=randread
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">read:
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  IOPS=1412.357544 BW(KiB/s)=5666
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  iops: min=1220 max=1724 avg=1427.666626
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  bw(KiB/s): min=4880 max=6896 avg=5711.066895
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"># Classic
</span></span><span class="line"><span class="ln">10</span><span class="cl">JobName: read_iops
</span></span><span class="line"><span class="ln">11</span><span class="cl">  blocksize=4K filesize=2G iodepth=64 rw=randread
</span></span><span class="line"><span class="ln">12</span><span class="cl">read:
</span></span><span class="line"><span class="ln">13</span><span class="cl">  IOPS=120.751854 BW(KiB/s)=498
</span></span><span class="line"><span class="ln">14</span><span class="cl">  iops: min=44 max=184 avg=123.064514
</span></span><span class="line"><span class="ln">15</span><span class="cl">  bw(KiB/s): min=176 max=736 avg=492.354828
</span></span></code></pre></div><p>Il est possible de sortir les résultats au format JSON si besoin.</p>
<h2 id="cloudnativepg-bench">CloudNativePG Bench</h2>
<p>En cherchant d'autres outils, je suis tombé sur l'opérateur <a href="https://cloudnative-pg.io/">CloudNativePG</a> qui permet de gérer des bases de données PSQL. Au premier abord aucun lien avec les tests de performances des disques. Sauf que le projet propose un plugin <code>kubectl</code>, avec la possibilité de benchmarker les disques sur lesquels pourront tourner les bases psql !</p>
<p>Le plugin <code>cnpg</code> utilise comme l'outil précédent <a href="https://cloudnative-pg.io/documentation/current/kubectl-plugin/#benchmarking-the-storage-with-fio">fio</a>, plus d'infos sur les options du plugin <a href="https://cloudnative-pg.io/documentation/current/benchmarking/#fio">ici</a>.</p>
<h3 id="installation-1">Installation</h3>
<p>Quand on parle de plugin <code>kubectl</code>, le meilleur moyen d'installer est d'utiliser <a href="https://krew.sigs.k8s.io/">krew</a>. On va donc utiliser cette méthode :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">$ kubectl krew install cnpg 
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">Updated the local copy of plugin index.
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">Installing plugin: cnpg
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">Installed plugin: cnpg
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">\
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"> | Use this plugin:
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"> |      kubectl cnpg
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"> | Documentation:
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"> |      https://github.com/cloudnative-pg/cloudnative-pg
</span></span><span class="line"><span class="ln">10</span><span class="cl">/
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">$ kubectl cnpg version  
</span></span><span class="line"><span class="ln">13</span><span class="cl">Build: {Version:1.21.0 Commit:9bc5b9b2 Date:2023-10-12}
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">$ kubectl cnpg fio --help
</span></span><span class="line"><span class="ln">16</span><span class="cl">Creates a fio deployment that will execute a fio job on the specified pvc.
</span></span></code></pre></div><h3 id="utilisation-1">Utilisation</h3>
<p>On retrouve sensiblement les mêmes options que <code>kubestr</code> pour tester les volumes :</p>
<ul>
<li><code>--pvcSize</code> : taille du PVC</li>
<li><code>--storageClass</code> : type de classe de stockage</li>
<li>Nom du job à fournir</li>
</ul>
<p>Ici pas de possibilité de fournir un fichier de configuration FIO. Par contre, l'option <code>--dry-run</code> permet de générer les manifests YAML qui seront utilisés pour le déploiement. On remarquera la création d'une ConfigMap qui contient les paramètres suivants :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ConfigMap</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">fio-job</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">job</span><span class="p">:</span><span class="w"> </span><span class="p">|-</span><span class="sd">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="sd">    [read]
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="sd">        direct=1
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="sd">        bs=8k
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="sd">        size=1G
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="sd">        time_based=1
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="sd">        runtime=60
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="sd">        ioengine=libaio
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="sd">        iodepth=32
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="sd">        end_fsync=1
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="sd">        log_avg_msec=1000
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="sd">        directory=/data
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="sd">        rw=read
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="sd">        write_bw_log=read
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="sd">        write_lat_log=read
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="sd">        write_iops_log=read</span><span class="w">
</span></span></span></code></pre></div><p>On pourra modifier alors cela si besoin de lancer des jobs <code>fio</code> différents.</p>
<p>On lance les 2 tests avec les mêmes caractéristiques que précédemment (les 2 StoragesClass, avec disque de 25Gb) :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">$ kubectl cnpg fio fio-job-high-speed --storageClass csi-cinder-high-speed --pvcSize 25Gi
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">Running this directly to the cluster may produce a disruption in the service, are you sure you want to proceed? (y/n)
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">y
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">PersistentVolumeClaim/fio-job-high-speed created
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">ConfigMap/fio-job-high-speed created
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">Deployment/fio-job-high-speed created
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">To remove this test you need to delete the Deployment, ConfigMap and PVC with the name fio-job-high-speed
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">The most simple way to do this is to re-run the command that was runto generate the deployment with the --dry-run flag and pipe that output to kubectl delete, e.g.:
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">kubectl cnpg fio &lt;fio-job-name&gt; --dry-run | kubectl delete -f 
</span></span></code></pre></div><p>Tiens ! Ici pas de résultats... Mais un déploiement qui se crée, fait sa vie, et ne sera pas arrêté à la fin du test. On devra supprimer le déploiement nous-même.</p>
<p>Lançons le 2ème test :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">$ kubectl cnpg fio fio-job-classic --storageClass csi-cinder-classic --pvcSize 25Gi 
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">Running this directly to the cluster may produce a disruption in the service, are you sure you want to proceed? (y/n)
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">y
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">PersistentVolumeClaim/fio-job-classic created
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">ConfigMap/fio-job-classic created
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">Deployment/fio-job-classic created
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">To remove this test you need to delete the Deployment, ConfigMap and PVC with the name fio-job-classic
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">The most simple way to do this is to re-run the command that was runto generate the deployment with the --dry-run flag and pipe that output to kubectl delete, e.g.:
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">kubectl cnpg fio &lt;fio-job-name&gt; --dry-run | kubectl delete -f -
</span></span></code></pre></div><p>Bon, on a bien nos 2 déploiements, avec mes 2 PVCs, il a dû se passer des &quot;trucs&quot; ?</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl get deploy,pvc
</span></span><span class="line"><span class="ln">2</span><span class="cl">NAME                                 READY   UP-TO-DATE   AVAILABLE   AGE
</span></span><span class="line"><span class="ln">3</span><span class="cl">deployment.apps/fio-job-classic      0/1     1            0           83s
</span></span><span class="line"><span class="ln">4</span><span class="cl">deployment.apps/fio-job-high-speed   1/1     1            1           5m17s
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl">NAME                                       STATUS   VOLUME                                                                   CAPACITY   ACCESS MODES   STORAGECLASS            AGE
</span></span><span class="line"><span class="ln">7</span><span class="cl">persistentvolumeclaim/fio-job-classic      Bound    ovh-managed-kubernetes-wqforj-pvc-fec4df30-b026-4f34-9dc2-6ad59dde805e   25Gi       RWO            csi-cinder-classic      83s
</span></span><span class="line"><span class="ln">8</span><span class="cl">persistentvolumeclaim/fio-job-high-speed   Bound    ovh-managed-kubernetes-wqforj-pvc-92f26269-6c0d-4953-9e5f-d4b4c88ab12e   25Gi       RWO            csi-cinder-high-speed   5m17s
</span></span></code></pre></div><h3 id="résultats-1">Résultats</h3>
<p>Pour voir les résultats, en fait le déploiement utilise <a href="https://github.com/wallnerryan/fio-tools">fio-tools</a> pour générer de &quot;beaux&quot; graphiques, et les données sont disponibles via un service web ! Il va falloir lancer un navigateur web pour voir cela, avec un coup de port-forwarding :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl port-forward deployment/fio-job-high-speed 8000
</span></span><span class="line"><span class="ln">2</span><span class="cl">Forwarding from 127.0.0.1:8000 -&gt; 8000
</span></span><span class="line"><span class="ln">3</span><span class="cl">Forwarding from [::1]:8000 -&gt; 8000
</span></span></code></pre></div><p>On va retrouver sur l'interface web :</p>
<ul>
<li>les résultats du job <code>fio</code> au format texte</li>
<li>les données de chaque job, au format brut/texte (fichiers .log)</li>
<li>des graphes visuels montrant les perfs au format PNG</li>
</ul>
<p>Voici, pour l'exemple, le résultat du précédent tests sur du disque &quot;high-speed&quot; :</p>
<p><img alt="Resultats FIO avec cnpg" class="zoomable" decoding="async" loading="lazy" src="/2023/10/2023-10-25-read_bw.1-2Dsmooth.webp"></p>
<h2 id="mes-remarques">Mes remarques</h2>
<p>Avec ces tests, j'ai pu confirmer que les gens qui se plaignaient au début avaient raison, et j'ai pu adapter le type de PVC pour coller au besoin applicatif. Remarque pour celles &amp; ceux travaillant sur des clusters OVHcloud, pensez à créer la StorageClass &quot;high-speed-gen2&quot; qui n'est pas activée par défaut ;)</p>
<p>Concernant les 2 outils, ma préférence pour les tests va au plugin <code>cpng</code>, qui avec la ConfigMap &amp; les graphes est plus sympa d'utilisation. Juste pensez-bien à supprimer les déploiements après tests.</p>
<p>Pour <code>kubestr</code>, ne pas négliger les autres options de l'outil comme <code>kubestr browse</code> qui permet de créer un snapshot de volume et naviguer dedans, très pratique !</p>]]></content:encoded></item><item><title>Migration Keycloak Quarkus et Certificats TLS LDAP</title><link>https://www.vrchr.fr/posts/2023/10/23/keycloak-ldap-tls/</link><pubDate>Mon, 23 Oct 2023 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2023/10/23/keycloak-ldap-tls/</guid><description>Comment gérer les certificats TLS LDAP avec Keycloak, Quarkus Edition</description><content:encoded><![CDATA[<p>J'utilise Keycloak depuis quelques temps sur un cluster kubernetes, installé via Helm, avec une version &quot;legacy&quot; basé sur wildfly. Celle-ci étant dépréciée depuis &quot;un moment&quot;, il a bien fallu mettre à jour.</p>
<p>Cependant, j'ai eu quelques soucis avec la gestion des certificats TLS lorsque keycloak doit fédérer des utilisateurs depuis un annuaire LDAP. Il y a pas mal de documentation sur le net, mais rien de bien clair.</p>
<p>Nous allons voir ci-après comment résoudre le problème, avec une configuration qui je pense est &quot;propre&quot;. Bon OK, pour le troll, Java, Truststore et propre je ne suis pas sûr que ça aille ensemble ^^.</p>
<h2 id="gestion-certificat-tls-mode-wildfly">Gestion Certificat TLS mode Wildfly</h2>
<p>Lorsque keycloak wildfly était déployé (via chart helm <a href="https://codecentric.github.io/helm-charts">codecentric</a>), la procédure était &quot;simple&quot; :</p>
<ol>
<li>Création d'une <code>ConfigMap</code> contenant tous les certificats des annuaires LDAP</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># Fichier keycloak-outgoing-ca-configmap.yaml </span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ConfigMap</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">keycloak-outgoing-ca</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">keycloak</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="c"># Certificat raccourci pour l&#39;exemple</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">ldap-server-1.crt</span><span class="p">:</span><span class="w"> </span><span class="p">|-</span><span class="sd">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="sd">    -----BEGIN CERTIFICATE-----
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="sd">    MIIFHDCCBASgAwIBAgISA97LrVW9BV88dCiH/SxYOguZMA0GCSqGSIb3DQEBCwUA
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="sd">    MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="sd">    EwJSMzAeFw0yMzA4MzEwNjE4MjdaFw0yMzExMjkwNjE4MjZaMBYxFDASBgNVBAMT
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="sd">    pKA0HK66WLqZC6dSqaEHvyM87L0BMgw0Y1sC12O6fHqcmTdtHMcbfzb98Row6ubX
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="sd">    Hgy6H75N33qgbC0ZqoYHsbZyJgs04enfix0FMd3qITJuwYeISve3zkFtKTAbZfos
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="sd">    qfAo6lbCsHF9/sv540yZ7A==
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="sd">    -----END CERTIFICATE-----</span><span class="w">
</span></span></span></code></pre></div><ol start="2">
<li>Référence de cette <code>ConfigMap</code> lors du déploiement du chart Helm, via l'utilisation de &quot;custom values&quot; :</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># Paramètres défini dans un fichier custom-values.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nt">extraVolumes</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="sd">  - name: keycloak-outgoing-ca
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="sd">    configMap:
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="sd">      name: keycloak-outgoing-ca</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nt">extraVolumeMounts</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="sd">  - name: keycloak-outgoing-ca
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="sd">    mountPath: /tmp/certs</span><span class="w">
</span></span></span></code></pre></div><ol start="3">
<li>Déploiement du chart :</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ helm repo add codecentric https://codecentric.github.io/helm-charts
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ helm repo update
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ helm upgrade --install keycloak codecentric/keycloak -f custom-values.yaml
</span></span></code></pre></div><h2 id="gestion-certificat-tls-mode-quarkus">Gestion Certificat TLS mode Quarkus</h2>
<p>Malheureusement, cette méthode ne fonctionne pas avec les déploiements actuels de Keycloak avec Quarkus, en utilisant le chart Bitnami ! Voir <a href="https://github.com/bitnami/charts/pull/10611#issuecomment-1152073620">ici</a> pour plus d'information.</p>
<p>Je ne comprends pas grand-chose dans les truststores and co., mais on me parle de &quot;Truststore SPI&quot; qui n'est pas paramétrable, qu'il faut passer par la surcharge de <code>JAVA_OPTS</code> etc. Pourtant, dans le chart Helm de Bitnami il y a bien une référence à du <a href="https://github.com/bitnami/charts/tree/main/bitnami/keycloak#keycloak-parameters">SPI</a>, on doit en tirer quelque chose !</p>
<h3 id="configuration-du-chart">Configuration du chart</h3>
<p>Après quelques essais, voici une configuration qui fonctionne, et qui me semble pas trop mal :</p>
<ol>
<li>Génération d'un truststore, contenant l'ensemble des certificats TLS des serveurs LDAP que je souhaite joindre. Un mot de passe devra être créé pour sécuriser l'accès au truststore. Pour éviter tout problème, j'utilise la même image de conteneur que ce qui tournera sur mon cluster (ici <code>19.0.3</code>) :</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="o">$</span> <span class="n">docker</span> <span class="n">run</span> <span class="o">-</span><span class="n">it</span> <span class="o">--</span><span class="n">rm</span> <span class="o">-</span><span class="n">v</span> <span class="o">./</span><span class="p">:</span><span class="o">/</span><span class="n">tmp</span> <span class="n">docker</span><span class="o">.</span><span class="n">io</span><span class="o">/</span><span class="n">bitnami</span><span class="o">/</span><span class="n">keycloak</span><span class="p">:</span><span class="mf">19.0</span><span class="o">.</span><span class="mi">3</span> <span class="n">bash</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">keycloak</span><span class="err">@</span><span class="mi">25</span><span class="n">d1c6186533</span><span class="p">:</span><span class="o">/$</span> <span class="n">cd</span> <span class="o">/</span><span class="n">tmp</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">keycloak</span><span class="err">@</span><span class="mi">25</span><span class="n">d1c6186533</span><span class="p">:</span><span class="o">/</span><span class="n">tmp</span><span class="o">$</span> <span class="n">openssl</span> <span class="n">x509</span> <span class="o">-</span><span class="n">text</span> <span class="o">-</span><span class="n">noout</span> <span class="o">-</span><span class="ow">in</span> <span class="n">ldap</span><span class="o">-</span><span class="n">server</span><span class="o">-</span><span class="mf">1.</span><span class="n">crt</span> 
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">keycloak</span><span class="err">@</span><span class="mi">25</span><span class="n">d1c6186533</span><span class="p">:</span><span class="o">/</span><span class="n">tmp</span><span class="o">$</span> <span class="n">keytool</span> <span class="o">-</span><span class="n">import</span> <span class="o">-</span><span class="n">alias</span> <span class="n">ldap</span><span class="o">.</span><span class="mf">192.168</span><span class="o">.</span><span class="mf">1.1</span><span class="o">.</span><span class="n">nip</span><span class="o">.</span><span class="n">io</span> <span class="o">-</span><span class="n">keystore</span> <span class="n">truststore</span><span class="o">.</span><span class="n">jks</span> <span class="o">-</span><span class="n">file</span> <span class="n">ldap</span><span class="o">-</span><span class="n">server</span><span class="o">-</span><span class="mf">1.</span><span class="n">crt</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">keycloak</span><span class="err">@</span><span class="mi">25</span><span class="n">d1c6186533</span><span class="p">:</span><span class="o">/</span><span class="n">tmp</span><span class="o">$</span> <span class="n">openssl</span> <span class="n">x509</span> <span class="o">-</span><span class="n">text</span> <span class="o">-</span><span class="n">noout</span> <span class="o">-</span><span class="ow">in</span> <span class="n">ldap</span><span class="o">-</span><span class="n">server</span><span class="o">-</span><span class="mf">2.</span><span class="n">crt</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">keycloak</span><span class="err">@</span><span class="mi">25</span><span class="n">d1c6186533</span><span class="p">:</span><span class="o">/</span><span class="n">tmp</span><span class="o">$</span> <span class="n">keytool</span> <span class="o">-</span><span class="n">import</span> <span class="o">-</span><span class="n">alias</span> <span class="n">ldap</span><span class="o">.</span><span class="mf">192.168</span><span class="o">.</span><span class="mf">1.2</span><span class="o">.</span><span class="n">nip</span><span class="o">.</span><span class="n">io</span> <span class="o">-</span><span class="n">keystore</span> <span class="n">truststore</span><span class="o">.</span><span class="n">jks</span> <span class="o">-</span><span class="n">file</span> <span class="n">ldap</span><span class="o">-</span><span class="n">server</span><span class="o">-</span><span class="mf">2.</span><span class="n">crt</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">keycloak</span><span class="err">@</span><span class="mi">25</span><span class="n">d1c6186533</span><span class="p">:</span><span class="o">/</span><span class="n">tmp</span><span class="o">$</span> <span class="n">keytool</span> <span class="o">-</span><span class="n">list</span> <span class="o">-</span><span class="n">keystore</span> <span class="n">truststore</span><span class="o">.</span><span class="n">jks</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">Enter</span> <span class="n">keystore</span> <span class="n">password</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">Keystore</span> <span class="n">type</span><span class="p">:</span> <span class="n">PKCS12</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">Keystore</span> <span class="n">provider</span><span class="p">:</span> <span class="n">SUN</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">Your</span> <span class="n">keystore</span> <span class="n">contains</span> <span class="mi">2</span> <span class="n">entries</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">ldap</span><span class="o">.</span><span class="mf">192.168</span><span class="o">.</span><span class="mf">1.1</span><span class="o">.</span><span class="n">nip</span><span class="o">.</span><span class="n">io</span><span class="p">,</span> <span class="n">Oct</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">2023</span><span class="p">,</span> <span class="n">trustedCertEntry</span><span class="p">,</span> 
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">Certificate</span> <span class="n">fingerprint</span> <span class="p">(</span><span class="n">SHA</span><span class="o">-</span><span class="mi">256</span><span class="p">):</span> <span class="mi">13</span><span class="p">:</span><span class="n">F3</span><span class="p">:</span><span class="n">F8</span><span class="p">:</span><span class="mi">65</span><span class="p">:</span><span class="mi">90</span><span class="p">:</span><span class="n">C6</span><span class="p">:</span><span class="mi">8</span><span class="n">A</span><span class="p">:</span><span class="n">EC</span><span class="p">:</span><span class="n">F7</span><span class="p">:</span><span class="mi">86</span><span class="p">:</span><span class="n">B9</span><span class="p">:</span><span class="n">E7</span><span class="p">:</span><span class="mi">82</span><span class="p">:</span><span class="n">E4</span><span class="p">:</span><span class="mi">4</span><span class="n">B</span><span class="p">:</span><span class="mi">09</span><span class="p">:</span><span class="n">C0</span><span class="p">:</span><span class="n">E8</span><span class="p">:</span><span class="n">B0</span><span class="p">:</span><span class="n">E4</span><span class="p">:</span><span class="n">A4</span><span class="p">:</span><span class="n">AD</span><span class="p">:</span><span class="mi">1</span><span class="n">C</span><span class="p">:</span><span class="mi">76</span><span class="p">:</span><span class="mi">7</span><span class="n">F</span><span class="p">:</span><span class="n">B6</span><span class="p">:</span><span class="n">CE</span><span class="p">:</span><span class="mi">56</span><span class="p">:</span><span class="mi">1</span><span class="n">B</span><span class="p">:</span><span class="mi">55</span><span class="p">:</span><span class="mi">51</span><span class="p">:</span><span class="mi">8</span><span class="n">F</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">ldap</span><span class="o">.</span><span class="mf">192.168</span><span class="o">.</span><span class="mf">1.2</span><span class="o">.</span><span class="n">nip</span><span class="o">.</span><span class="n">io</span><span class="p">,</span> <span class="n">Oct</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">2023</span><span class="p">,</span> <span class="n">trustedCertEntry</span><span class="p">,</span> 
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">Certificate</span> <span class="n">fingerprint</span> <span class="p">(</span><span class="n">SHA</span><span class="o">-</span><span class="mi">256</span><span class="p">):</span> <span class="n">A2</span><span class="p">:</span><span class="n">B2</span><span class="p">:</span><span class="mi">01</span><span class="p">:</span><span class="mi">9</span><span class="n">D</span><span class="p">:</span><span class="mi">7</span><span class="n">B</span><span class="p">:</span><span class="mi">87</span><span class="p">:</span><span class="n">FD</span><span class="p">:</span><span class="mi">04</span><span class="p">:</span><span class="mi">7</span><span class="n">F</span><span class="p">:</span><span class="n">F2</span><span class="p">:</span><span class="mi">98</span><span class="p">:</span><span class="n">AF</span><span class="p">:</span><span class="mi">28</span><span class="p">:</span><span class="mi">80</span><span class="p">:</span><span class="n">B6</span><span class="p">:</span><span class="n">CE</span><span class="p">:</span><span class="mi">9</span><span class="n">B</span><span class="p">:</span><span class="mi">8</span><span class="n">F</span><span class="p">:</span><span class="mi">73</span><span class="p">:</span><span class="mi">88</span><span class="p">:</span><span class="mi">9</span><span class="n">A</span><span class="p">:</span><span class="mi">5</span><span class="n">A</span><span class="p">:</span><span class="n">DE</span><span class="p">:</span><span class="mi">6</span><span class="n">D</span><span class="p">:</span><span class="mi">82</span><span class="p">:</span><span class="n">D3</span><span class="p">:</span><span class="mi">33</span><span class="p">:</span><span class="mi">18</span><span class="p">:</span><span class="mi">85</span><span class="p">:</span><span class="n">BA</span><span class="p">:</span><span class="mi">66</span><span class="p">:</span><span class="mi">25</span>
</span></span></code></pre></div><ol start="2">
<li>Génération d'un secret contenant ce truststore. Le contenu du secret devra être dans <code>keycloak-spi.truststore.jks</code> :</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl create secret generic keycloak-spi-store --from-file=keycloak-spi.truststore.jks=truststore.jks
</span></span></code></pre></div><ol start="3">
<li>Il est possible également de créer ce secret directement en les définissant dans les values du chart Helm (optionnel) :</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">extraDeploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="c">### Creates truststore used for validating certs for outgoing connexions (LDAPs)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span>- <span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="sd">    apiVersion: v1
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="sd">    kind: Secret
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="sd">    metadata:
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="sd">      name: {{ .Values.spi.existingSecret }}
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="sd">    data:
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="sd">      keycloak-spi.truststore.jks: &lt;BASE_64_FILE&gt;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span></code></pre></div><ol start="4">
<li>Reste à paramétrer le chart Helm pour utiliser ce truststore. On nous parle de variables d'environnement <code>KEYCLOAK_SPI_TRUSTSTORE_FILE</code>, <code>KEYCLOAK_SPI_TRUSTSTORE_PASSWORD</code> ou encore <code>KEYCLOAK_SPI_TRUSTSTORE_FILE_HOSTNAME_VERIFICATION_POLICY</code>, celles-ci seront définies via les paramètres du chart (le contenu des values est disponible <a href="https://github.com/bitnami/charts/blob/main/bitnami/keycloak/values.yaml#L175">ici</a>) :</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># Extract du fichier custom-values.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c">## SPI TLS settings</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">spi</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">existingSecret</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;keycloak-spi-store&#34;</span><span class="w"> </span><span class="c"># Nom du secret contenant le truststore</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">truststorePassword</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;changeme&#34;</span><span class="w"> </span><span class="c"># Mot de passe du truststore pour l&#39;exemple, en production on utilisera un secret, à définir dans passwordsSecret</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">truststoreFilename</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;keycloak-spi.truststore.jks&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">passwordsSecret</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="w"> </span><span class="c"># Voir commentaires pour truststorePassword</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">hostnameVerificationPolicy</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="l">STRICT&#34;</span><span class="w"> </span><span class="c"># On vérifie le nom du serveur portant le certificat</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span></code></pre></div><ol start="5">
<li>On déploie le tout !</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ helm repo add bitnami https://charts.bitnami.com/bitnami
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ helm repo update
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ helm upgrade --install keycloak bitnami/keycloak --namespace keycloak --values custom-values.yaml
</span></span></code></pre></div><h3 id="vérification">Vérification</h3>
<p>Maintenant, il ne reste plus qu'à vérifier sur Keycloak que la connexion est OK.</p>
<p><img alt="Configuration Keycloak LDAP TLS" class="zoomable" decoding="async" loading="lazy" src="/2023/10/2023-10-23-keycloak-ldap-config.webp"></p>
<p>Allons dans la partie &quot;User federation&quot; du royaume qui utilise le lien vers mes serveurs LDAP, testons la connexion... Bingo !</p>
<p><img alt="Liaison Keycloak LDAP OK" class="zoomable" decoding="async" loading="lazy" src="/2023/10/2023-10-23-keycloak-ldap-ok.webp"></p>]]></content:encoded></item><item><title>Ajouter une entrée DNS custom à coredns</title><link>https://www.vrchr.fr/posts/2023/10/18/coredns/</link><pubDate>Wed, 18 Oct 2023 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2023/10/18/coredns/</guid><description>Ajouter une entrée DNS custom à coredns</description><content:encoded><![CDATA[<p>Ci-après un article pense-bête pour modifier la configuration de coredns, le serveur DNS que l'on retrouvera par défaut dans un cluster Kubernetes.</p>
<p>Il m'arrive parfois de devoir ajouter des entrées DNS à la main dans un cluster, pour que les applications de celui-ci puissent joindre un service non encore déclaré, ou bien pour pointer vers un service répliqué hors prod. En gros modifier le <code>/etc/hosts</code> de sa machine linux, mais pour l'ensemble des applis du cluster.</p>
<p>Ici, je souhaite que mes applis puissent joindre <code>myapp.opsrel.io</code> sur l'IP <code>74.220.17.133</code>, on va voir comment coredns permet cela.</p>
<p>Pour l'exemple, j'utilise un Cluster Civo, qui me permet de provisionner des clusters facilement. Les modifications à faire sur coredns peuvent changer selon l'installation faite par votre cloud provider ou admin préféré.</p>
<h2 id="création-du-cluster-de-test">Création du cluster de test</h2>
<p>Il suffit simplement d'utiliser la CLI pour provisionner le cluster. Ici je crée un cluster de 2 nœuds avec des instances <code>xsmall</code>. Je récupère ensuite le fichier de configuration pour pouvoir m'y connecter (via la variable d'environnement <code>KUBECONFIG</code>).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="ln">1</span><span class="cl"><span class="o">$</span> <span class="n">civo</span> <span class="n">kubernetes</span> <span class="n">create</span> <span class="n">coredns</span><span class="o">-</span><span class="n">demo</span> <span class="o">--</span><span class="n">size</span><span class="o">=</span><span class="n">g4s</span><span class="o">.</span><span class="n">kube</span><span class="o">.</span><span class="n">xsmall</span> <span class="o">--</span><span class="n">nodes</span><span class="o">=</span><span class="mi">2</span> <span class="o">--</span><span class="n">wait</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">$</span> <span class="n">civo</span> <span class="n">kubernetes</span> <span class="n">config</span> <span class="n">coredns</span><span class="o">-</span><span class="n">demo</span> <span class="o">-</span><span class="n">s</span> <span class="o">-</span><span class="n">p</span> <span class="n">coredns</span><span class="o">-</span><span class="n">demo</span><span class="o">-</span><span class="n">kubeconfig</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="o">$</span> <span class="k">export</span> <span class="n">KUBECONFIG</span><span class="o">=$</span><span class="p">{</span><span class="n">PWD</span><span class="p">}</span><span class="o">/</span><span class="n">coredns</span><span class="o">-</span><span class="n">demo</span><span class="o">-</span><span class="n">kubeconfig</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="o">$</span> <span class="n">kubectl</span> <span class="n">get</span> <span class="n">nodes</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">NAME</span>                                                <span class="n">STATUS</span>   <span class="n">ROLES</span>    <span class="n">AGE</span>     <span class="n">VERSION</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">k3s</span><span class="o">-</span><span class="n">coredns</span><span class="o">-</span><span class="n">demo</span><span class="o">-</span><span class="mi">4</span><span class="n">eed</span><span class="o">-</span><span class="n">ef4cb7</span><span class="o">-</span><span class="n">node</span><span class="o">-</span><span class="n">pool</span><span class="o">-</span><span class="n">eb1c</span><span class="o">-</span><span class="n">fuv8f</span>   <span class="n">Ready</span>    <span class="o">&lt;</span><span class="n">none</span><span class="o">&gt;</span>   <span class="mi">5</span><span class="n">m27s</span>   <span class="n">v1</span><span class="o">.</span><span class="mf">26.4</span><span class="o">+</span><span class="n">k3s1</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">k3s</span><span class="o">-</span><span class="n">coredns</span><span class="o">-</span><span class="n">demo</span><span class="o">-</span><span class="mi">4</span><span class="n">eed</span><span class="o">-</span><span class="n">ef4cb7</span><span class="o">-</span><span class="n">node</span><span class="o">-</span><span class="n">pool</span><span class="o">-</span><span class="n">eb1c</span><span class="o">-</span><span class="n">o9bkt</span>   <span class="n">Ready</span>    <span class="o">&lt;</span><span class="n">none</span><span class="o">&gt;</span>   <span class="mi">5</span><span class="n">m52s</span>   <span class="n">v1</span><span class="o">.</span><span class="mf">26.4</span><span class="o">+</span><span class="n">k3s1</span>
</span></span></code></pre></div><h2 id="vérification-de-la-configuration">Vérification de la configuration</h2>
<p>Tout d'abord, analysons la configuration <code>coredns</code>. J'ai un déploiement actif, et en regardant de plus près, je vois 2 volumes utilisés pour sa configuration :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">$ kubectl get deployments coredns -n kube-system
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">NAME      READY   UP-TO-DATE   AVAILABLE   AGE
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">coredns   1/1     1            1           3h25m
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">$ kubectl describe deployments coredns -n kube-system
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">Name:                   coredns
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">Namespace:              kube-system
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">[..]
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">Pod Template:
</span></span><span class="line"><span class="ln">10</span><span class="cl">  [...]
</span></span><span class="line"><span class="ln">11</span><span class="cl">  Containers:
</span></span><span class="line"><span class="ln">12</span><span class="cl">   coredns:
</span></span><span class="line"><span class="ln">13</span><span class="cl">    [...]
</span></span><span class="line"><span class="ln">14</span><span class="cl">    Mounts:
</span></span><span class="line"><span class="ln">15</span><span class="cl">      /etc/coredns from config-volume (ro)
</span></span><span class="line"><span class="ln">16</span><span class="cl">      /etc/coredns/custom from custom-config-volume (ro)
</span></span><span class="line"><span class="ln">17</span><span class="cl">  Volumes:
</span></span><span class="line"><span class="ln">18</span><span class="cl">   config-volume:
</span></span><span class="line"><span class="ln">19</span><span class="cl">    Type:      ConfigMap (a volume populated by a ConfigMap)
</span></span><span class="line"><span class="ln">20</span><span class="cl">    Name:      coredns
</span></span><span class="line"><span class="ln">21</span><span class="cl">    Optional:  false
</span></span><span class="line"><span class="ln">22</span><span class="cl">   custom-config-volume:
</span></span><span class="line"><span class="ln">23</span><span class="cl">    Type:                       ConfigMap (a volume populated by a ConfigMap)
</span></span><span class="line"><span class="ln">24</span><span class="cl">    Name:                       coredns-custom
</span></span><span class="line"><span class="ln">25</span><span class="cl">    Optional:                   true
</span></span><span class="line"><span class="ln">26</span><span class="cl">[...]
</span></span></code></pre></div><p>Listons ces ConfigMaps :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl get configmap -n kube-system
</span></span><span class="line"><span class="ln">2</span><span class="cl">NAME                                 DATA   AGE
</span></span><span class="line"><span class="ln">3</span><span class="cl">[...]
</span></span><span class="line"><span class="ln">4</span><span class="cl">coredns                              2      3h28m
</span></span><span class="line"><span class="ln">5</span><span class="cl">[...]
</span></span></code></pre></div><p>Seule la ConfigMap par défaut est là, <code>coredns-custom</code> n'existe pas, il faudra donc la créer pour rajouter les infos. Dans la configuration par défaut, on retrouvera la <a href="https://coredns.io/2017/07/23/corefile-explained/">Corefile</a> ainsi que les <code>NodeHosts</code> dans le cas de Civo.</p>
<h2 id="création-dune-entrée-custom-statique">Création d'une entrée custom statique</h2>
<p>Pour rajouter une entrée statique, on ne va surtout pas toucher à la configuration de base, qui pourrait se faire écraser lors d'un redéploiement. Si l'on regarde la ConfigMap <code>coredns</code>, on voit que la Corefile importe les fichiers présents dans <code>/etc/coredns/custom/*.server</code> (qui correspond au point de montage de la ConfigMap custom):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl get configmaps coredns -n kube-system -o yaml
</span></span><span class="line"><span class="ln">2</span><span class="cl">apiVersion: v1
</span></span><span class="line"><span class="ln">3</span><span class="cl">data:
</span></span><span class="line"><span class="ln">4</span><span class="cl">  Corefile: |
</span></span><span class="line"><span class="ln">5</span><span class="cl">    [...]
</span></span><span class="line"><span class="ln">6</span><span class="cl">    import /etc/coredns/custom/*.server
</span></span><span class="line"><span class="ln">7</span><span class="cl">[...]
</span></span></code></pre></div><p>Reste à créer la ConfigMap <code>coredns-custom</code> avec les entrées DNS souhaitées :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># Fichier coredns-custom.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ConfigMap</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">coredns-custom</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">kube-system</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">opsrel.server</span><span class="p">:</span><span class="w"> </span><span class="p">|-</span><span class="sd">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="sd">    # Include specific server zone
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="sd">    opsrel.io. {
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="sd">      errors
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="sd">      forward . /etc/resolv.conf
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="sd">      hosts {
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="sd">        74.220.17.133 myapp.opsrel.io.
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="sd">        fallthrough
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="sd">      }
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="sd">    }</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl apply -f coredns-custom.yaml -n kube-system
</span></span></code></pre></div><p>On redémarre coredns pour la prise en compte :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl rollout restart deployment coredns -n kube-system
</span></span><span class="line"><span class="ln">2</span><span class="cl">deployment.apps/coredns restarted
</span></span></code></pre></div><h2 id="validation-des-modifications">Validation des modifications</h2>
<p>Maintenant, testons sur un pod simple que la résolution fonctionne :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl run debug --image=nginx
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ kubectl exec -it debug  -- /bin/bash
</span></span><span class="line"><span class="ln">3</span><span class="cl">root@debug:/# apt-get update &amp;&amp; apt-get install bind9-dnsutils -y
</span></span><span class="line"><span class="ln">4</span><span class="cl">root@debug:/# nslookup myapp.opsrel.io
</span></span><span class="line"><span class="ln">5</span><span class="cl">Server:         10.43.0.10
</span></span><span class="line"><span class="ln">6</span><span class="cl">Address:        10.43.0.10#53
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl">Name:   myapp.opsrel.io
</span></span><span class="line"><span class="ln">9</span><span class="cl">Address: 74.220.17.133
</span></span></code></pre></div><p>Je vérifie que j'ai pas tout cassé :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">root@debug:/# nslookup opsrel.io
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">Server:         10.43.0.10
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">Address:        10.43.0.10#53
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">Non-authoritative answer:
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">Name:   opsrel.io
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">Address: 213.186.33.5
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">root@debug:/# nslookup metrics-server
</span></span><span class="line"><span class="ln">10</span><span class="cl">Server:         10.43.0.10
</span></span><span class="line"><span class="ln">11</span><span class="cl">Address:        10.43.0.10#53
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">Name:   metrics-server.kube-system.svc.cluster.local
</span></span><span class="line"><span class="ln">14</span><span class="cl">Address: 10.43.123.103
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">root@debug:/# nslookup www.debian.org 
</span></span><span class="line"><span class="ln">17</span><span class="cl">Server:         10.43.0.10
</span></span><span class="line"><span class="ln">18</span><span class="cl">Address:        10.43.0.10#53
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">Non-authoritative answer:
</span></span><span class="line"><span class="ln">21</span><span class="cl">Name:   www.debian.org
</span></span><span class="line"><span class="ln">22</span><span class="cl">Address: 130.89.148.77
</span></span><span class="line"><span class="ln">23</span><span class="cl">Name:   www.debian.org
</span></span><span class="line"><span class="ln">24</span><span class="cl">Address: 2001:67c:2564:a119::77
</span></span></code></pre></div><p>Super ! J'ai mon entrée DNS statique qui fonctionne, je vais pouvoir continuer mes tests !</p>]]></content:encoded></item><item><title>Accéder à une ressource externe via un Ingress Controller et Certificat TLS valide</title><link>https://www.vrchr.fr/posts/2023/10/16/ingress-external-https/</link><pubDate>Mon, 16 Oct 2023 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2023/10/16/ingress-external-https/</guid><description>Accéder à une ressource externe via un Ingress Controller et Certificat TLS valide</description><content:encoded><![CDATA[<p>Pour un besoin particulier, j'ai dû chercher une solution pour accéder à du contenu d'un bucket S3 public, depuis le même domaine qu'une application tournant sur un cluster Kubernetes. Les données devant être accessibles en HTTPS avec un certificat valide.</p>
<p>Le bucket S3 étant chez <a href="https://www.ovhcloud.com/fr/">OVHcloud</a>, il existe des solutions pour cela, notamment <a href="https://help.ovhcloud.com/csm/fr-public-cloud-storage-s3-static-website-https?id=kb_article_view&amp;sysparm_article=KB0058097">celle-ci</a>. Cependant, j'utilise Let's Encrypt pour gérer mes certificats, j'ai déjà un load balancer et tout ce qu'il faut sur mes clusters kubernetes, il y a surement un moyen de faire autrement ?</p>
<p>Dans cet article, je vais alors tenter de récupérer un fichier <code>git-cheat-sheet-education.pdf</code>, hébergé sur un bucket S3 public, mais via une URL d'un cluster kubernetes, <code>https://demo-ingress-s3.opsrel.io</code>. Voici alors une solution proposée parmi d'autres, qui utilisera les concepts suivants :</p>
<ol>
<li>Bucket S3 avec données accessibles publiquement</li>
<li>Ingress Controller (ici <a href="https://kubernetes.github.io/ingress-nginx/">ingress-nginx</a>) sur un cluster Kubernetes</li>
<li>Let's Encrypt avec la génération de certificat TLS valide</li>
<li>Un nom de domaine ;)</li>
</ol>
<p>Ci-après un schéma simple du cas d'usage :</p>
<p><img alt="Requête HTTP via Ingress Controller et service ExternalName" class="zoomable" decoding="async" loading="lazy" src="/2023/10/2023-10-16-ingress-external-https-schema.webp"></p>
<p>Je ne traiterai pas ici la création d'un bucket S3 ni la gestion des ACLs. Chez OVHcloud, vous trouverez des informations <a href="https://help.ovhcloud.com/csm/fr-public-cloud-storage-s3-bucket-acl?id=kb_article_view&amp;sysparm_article=KB0057088">ici</a>.</p>
<p>Je ne traiterai pas non plus l'installation d'ingress-nginx et Let's Encrypt !</p>
<p>Mais du coup, de quoi vais-je traiter ? Eh bien simplement du lien ingress-nginx -&gt; bucket S3 grâce aux ressources <a href="https://kubernetes.io/docs/concepts/services-networking/service/#externalname">External Names</a> de Kubernetes !</p>
<h2 id="vérification-des-pré-requis">Vérification des pré-requis</h2>
<p>Avant d'aller plus loin, vérifions que mon ingress controller et Let's Encrypt sont bien installés :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">$ helm list -n ingress-nginx
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">NAME         	NAMESPACE    	REVISION	UPDATED                                 	STATUS  	CHART              	APP VERSION
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">ingress-nginx	ingress-nginx	2       	2023-10-16 21:58:04.555376832 +0200 CEST	deployed	ingress-nginx-4.8.2	1.9.3  
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">$ kubectl get svc ingress-nginx -n ingress-nginx
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">NAME                                 TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)                      AGE
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">ingress-nginx-controller-admission   ClusterIP      10.43.34.243   &lt;none&gt;          443/TCP                      5m17s
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">ingress-nginx-controller             LoadBalancer   10.43.12.211   74.220.23.199   80:30198/TCP,443:31639/TCP   5m17s
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">$ helm list -n cert-manager 
</span></span><span class="line"><span class="ln">11</span><span class="cl">NAME        	NAMESPACE   	REVISION	UPDATED                                 	STATUS  	CHART               	APP VERSION
</span></span><span class="line"><span class="ln">12</span><span class="cl">cert-manager	cert-manager	1       	2023-10-16 22:02:52.440865101 +0200 CEST	deployed	cert-manager-v1.13.1	v1.13.1    
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">$ kubectl get deploy,pods -n cert-manager
</span></span><span class="line"><span class="ln">15</span><span class="cl">NAME                                      READY   UP-TO-DATE   AVAILABLE   AGE
</span></span><span class="line"><span class="ln">16</span><span class="cl">deployment.apps/cert-manager              1/1     1            1           79s
</span></span><span class="line"><span class="ln">17</span><span class="cl">deployment.apps/cert-manager-cainjector   1/1     1            1           79s
</span></span><span class="line"><span class="ln">18</span><span class="cl">deployment.apps/cert-manager-webhook      1/1     1            1           79s
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">NAME                                           READY   STATUS    RESTARTS   AGE
</span></span><span class="line"><span class="ln">21</span><span class="cl">pod/cert-manager-55657857dd-fw8f2              1/1     Running   0          79s
</span></span><span class="line"><span class="ln">22</span><span class="cl">pod/cert-manager-cainjector-7b5b5d4786-fnbcd   1/1     Running   0          79s
</span></span><span class="line"><span class="ln">23</span><span class="cl">pod/cert-manager-webhook-55fb5c9c88-kdktw      1/1     Running   0          79s
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl">$ kubectl get clusterissuer
</span></span><span class="line"><span class="ln">26</span><span class="cl">NAME               READY   AGE
</span></span><span class="line"><span class="ln">27</span><span class="cl">letsencrypt-prod   True    22s
</span></span></code></pre></div><p>OK, j'ai bien mon controller ingress-nginx et cert-manager qui tournent, avec une IP publique <code>74.220.23.199</code>.</p>
<p>Je vais utiliser l'entrée DNS <code>demo-ingress-s3.opsrel.io</code> qui va pointer vers cette IP. Pour le moment, la résolution fonctionne bien, mais le certificat par défaut du cluster est présenté :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">$ curl https://demo-ingress-s3.opsrel.io -vkI
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">*   Trying 74.220.23.199:443...
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">* Connected to demo-ingress-s3.opsrel.io (74.220.23.199) port 443 (#0)
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">* [...]
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">* Server certificate:
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">*  subject: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">*  start date: Oct 16 19:58:20 2023 GMT
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">*  expire date: Oct 15 19:58:20 2024 GMT
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">*  issuer: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate
</span></span><span class="line"><span class="ln">10</span><span class="cl">*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
</span></span><span class="line"><span class="ln">11</span><span class="cl">* [...]
</span></span><span class="line"><span class="ln">12</span><span class="cl">&lt; 
</span></span><span class="line"><span class="ln">13</span><span class="cl">* Connection #0 to host demo-ingress-s3.opsrel.io left intact
</span></span></code></pre></div><p>Le certificat est bien celui par défaut, <code>Kubernetes Ingress Controller Fake Certificate</code>, car aucune ressource n'est associée à ce nom de domaine.</p>
<p>Maintenant, passons à l'étape suivante, créons la ressource de type <strong>Ingress</strong> + <strong>ExternalName</strong>.</p>
<h2 id="ingress-et-external-name">Ingress et External Name</h2>
<p>On va maintenant configurer correctement ce qu'il faut pour récupérer le document <a href="http://demo-ingress-s3.auth-2bf97a4bab8c4d16969513edf37fcc15.storage.gra.cloud.ovh.net/git-cheat-sheet-education.pdf">git-cheat-sheet-education.pdf</a>, mais via une URL à moi, avec un bon certificat ;)</p>
<h3 id="service-externalname">Service ExternalName</h3>
<p>Kubernetes permet d'exposer des services au sein du cluster, pour joindre des ressources. Les plus connus sont les services de type <code>ClusterIP</code> ou <code>LoadBalancer</code>. Dans mon cas, je vais utiliser un service un peu particulier, <code>ExternalName</code>, qui permet d'aller chercher une ressource à l'extérieur du namespace où il se trouve, que ce soit dans le cluster (sur un autre ns) ou carrément en dehors du cluster (mon cas d'usage).</p>
<p>Avec cet <code>ExternalName</code>, je pourrai donc depuis le cluster accéder à une URL externe via ce service.</p>
<p>Voici le manifest yaml <code>demo-ingress-s3-upstream-svc.yaml</code> à appliquer :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">demo-ingress-s3-upstream</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="c"># FQDN de mon bucket S3</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="nt">externalName</span><span class="p">:</span><span class="w"> </span><span class="l">demo-ingress-s3.auth-2bf97a4bab8c4d16969513edf37fcc15.storage.gra.cloud.ovh.net</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">ExternalName</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl apply -f demo-ingress-s3-upstream-svc.yaml
</span></span><span class="line"><span class="ln">2</span><span class="cl">service/demo-ingress-s3-upstream created
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">$ kubectl get svc demo-ingress-s3-upstream
</span></span><span class="line"><span class="ln">5</span><span class="cl">NAME                       TYPE           CLUSTER-IP   EXTERNAL-IP                                                                       PORT(S)   AGE
</span></span><span class="line"><span class="ln">6</span><span class="cl">demo-ingress-s3-upstream   ExternalName   &lt;none&gt;       demo-ingress-s3.auth-2bf97a4bab8c4d16969513edf37fcc15.storage.gra.cloud.ovh.net   &lt;none&gt;    4s
</span></span></code></pre></div><p>Si maintenant je fais un port-forward sur ce service, je devrais pouvoir récupérer mon fichier.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl port-forward svc/demo-ingress-s3-upstream 80
</span></span><span class="line"><span class="ln">2</span><span class="cl">error: cannot attach to *v1.Service: invalid service &#39;demo-ingress-s3-upstream&#39;: Service is defined without a selector
</span></span></code></pre></div><p>Mmmm, ça ne veut pas marcher... En effet, le service n'a pas de selector et ne sait pas vers quel port/pod rediriger, ça ne fonctionnera pas ! Qu'à cela ne tienne, on va tenter depuis un pod à l'intérieur du cluster :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">$ kubectl run debug --image=nginx # on lance un conteneur nginx tout simple, pour avoir un shell ;)
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">$ kubectl exec -it debug -- /bin/bash
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">root@debug:/# curl http://demo-ingress-s3-upstream/git-cheat-sheet-education.pdf -I
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">HTTP/1.1 412 Precondition Failed
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">Content-Type: text/html; charset=UTF-8
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">Content-Length: 7
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">X-Trans-Id: tx7dc078f4c724468e89f50-00652ee47d
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">X-Openstack-Request-Id: tx7dc078f4c724468e89f50-00652ee47d
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">Date: Tue, 17 Oct 2023 19:46:05 GMT
</span></span><span class="line"><span class="ln">10</span><span class="cl">X-IPLB-Request-ID: 4ADC14C0:8FA4_5762BBC9:0050_652EE47D_5A23D18:1D243
</span></span><span class="line"><span class="ln">11</span><span class="cl">X-IPLB-Instance: 48126
</span></span></code></pre></div><p>Ça ne fonctionne toujours pas... C'est parce que le serveur en face s'attend à avoir une requête vers le vhost du bucket, et pas <code>demo-ingress-s3-upstream</code>. On retente en spécifiant le vhost :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">root@debug:/# curl http://demo-ingress-s3-upstream/git-cheat-sheet-education.pdf  -H &#34;Host: demo-ingress-s3.auth-2bf97a4bab8c4d16969513edf37fcc15.storage.gra.cloud.ovh.net&#34; -I
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">HTTP/1.1 200 OK
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">Content-Type: application/pdf
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">Etag: 1fcf70393d2aed298ef2c521126e8cd3
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">Last-Modified: Tue, 17 Oct 2023 19:23:45 GMT
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">X-Timestamp: 1697570624.72872
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">Accept-Ranges: bytes
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">Content-Length: 100194
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">X-Trans-Id: tx74195141890c422cba15c-00652ee50b
</span></span><span class="line"><span class="ln">10</span><span class="cl">X-Openstack-Request-Id: tx74195141890c422cba15c-00652ee50b
</span></span><span class="line"><span class="ln">11</span><span class="cl">Date: Tue, 17 Oct 2023 19:48:27 GMT
</span></span><span class="line"><span class="ln">12</span><span class="cl">X-IPLB-Request-ID: 4ADC14C0:7995_3626E64B:0050_652EE50B_4400B4E:71FE
</span></span><span class="line"><span class="ln">13</span><span class="cl">X-IPLB-Instance: 33617
</span></span></code></pre></div><p>Ok ! On a bien un <code>200 OK</code>, essayant de télécharger mon PDF (<code>Content-Type: application/pdf</code>).</p>
<p>Par contre, si j'essaie de récupérer le document en HTTPS, j'ai l'erreur suivante, ce qui est normal car le certificat présenté ne matche pas avec l'URL demandée :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">root@debug:/# curl https://demo-ingress-s3-upstream/git-cheat-sheet-education.pdf -I
</span></span><span class="line"><span class="ln">2</span><span class="cl">curl: (60) SSL: no alternative certificate subject name matches target host name &#39;demo-ingress-s3-upstream&#39;
</span></span></code></pre></div><h3 id="ingress">Ingress</h3>
<p>Maintenant, reste à paramétrer l'ingress pour faire le lien depuis l'extérieur, vers ce service spécifique.</p>
<p>On crée une ressource de type ingress, <code>demo-ingress-s3-upstream-ingress.yaml</code>, avec les informations suivantes :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="c"># Ressource cert-manager pour demander des certificat Let&#39;s Encrypt, non traité ici</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">cert-manager.io/cluster-issuer</span><span class="p">:</span><span class="w"> </span><span class="l">letsencrypt-prod</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="c"># On définit le vhost pour éviter le problème vu plus haut</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/upstream-vhost</span><span class="p">:</span><span class="w"> </span><span class="l">demo-ingress-s3.auth-2bf97a4bab8c4d16969513edf37fcc15.storage.gra.cloud.ovh.net</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">demo-ingress-s3-upstream</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">ingressClassName</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">demo-ingress-s3.opsrel.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">      </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">      </span>- <span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">          </span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">            </span><span class="c"># On pointe vers le service externalName définit plus haut</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">demo-ingress-s3-upstream</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">            </span><span class="nt">port</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">              </span><span class="c"># Par défaut, le lien upstream se fait en HTTP, port 80</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">              </span><span class="nt">number</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">        </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">        </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">ImplementationSpecific</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">  </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">    </span>- <span class="l">demo-ingress-s3.opsrel.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">    </span><span class="c"># On stocke le certificat TLS dans le secret associé</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">    </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">demo-ingress-s3-cert</span><span class="w">
</span></span></span></code></pre></div><p>On applique le manifest et on vérifie que tout est bon :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl apply -f demo-ingress-s3-upstream-ingress.yaml
</span></span><span class="line"><span class="ln">2</span><span class="cl">ingress.networking.k8s.io/demo-ingress-s3-upstream created
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">$ kubectl get ingress,certificate                                                                                                        
</span></span><span class="line"><span class="ln">5</span><span class="cl">NAME                                                 CLASS   HOSTS                       ADDRESS         PORTS     AGE
</span></span><span class="line"><span class="ln">6</span><span class="cl">ingress.networking.k8s.io/demo-ingress-s3-upstream   nginx   demo-ingress-s3.opsrel.io   74.220.23.199   80, 443   2m19s
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl">NAME                                               READY   SECRET                 AGE
</span></span><span class="line"><span class="ln">9</span><span class="cl">certificate.cert-manager.io/demo-ingress-s3-cert   True    demo-ingress-s3-cert   2m19s
</span></span></code></pre></div><p>Ok, reste plus qu'à tester :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">$ curl https://demo-ingress-s3.opsrel.io/git-cheat-sheet-education.pdf -vI 
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">*   Trying 74.220.23.199:443...
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">* Connected to demo-ingress-s3.opsrel.io (74.220.23.199) port 443 (#0)
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">* [...]
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">* Server certificate:
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">*  subject: CN=demo-ingress-s3.opsrel.io
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">*  start date: Oct 17 18:53:20 2023 GMT
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">*  expire date: Jan 15 18:53:19 2024 GMT
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">*  subjectAltName: host &#34;demo-ingress-s3.opsrel.io&#34; matched cert&#39;s &#34;demo-ingress-s3.opsrel.io&#34;
</span></span><span class="line"><span class="ln">10</span><span class="cl">*  issuer: C=US; O=Let&#39;s Encrypt; CN=R3
</span></span><span class="line"><span class="ln">11</span><span class="cl">*  SSL certificate verify ok.
</span></span><span class="line"><span class="ln">12</span><span class="cl">* [...]
</span></span><span class="line"><span class="ln">13</span><span class="cl">&lt; HTTP/2 200
</span></span><span class="line"><span class="ln">14</span><span class="cl">HTTP/2 200
</span></span><span class="line"><span class="ln">15</span><span class="cl">&lt; date: Tue, 17 Oct 2023 19:59:38 GMT
</span></span><span class="line"><span class="ln">16</span><span class="cl">date: Tue, 17 Oct 2023 19:59:38 GM
</span></span><span class="line"><span class="ln">17</span><span class="cl">&lt; content-type: application/pdf
</span></span><span class="line"><span class="ln">18</span><span class="cl">content-type: application/pdf
</span></span><span class="line"><span class="ln">19</span><span class="cl">&lt; content-length: 100194
</span></span><span class="line"><span class="ln">20</span><span class="cl">content-length: 100194
</span></span><span class="line"><span class="ln">21</span><span class="cl">[...]
</span></span></code></pre></div><p>Cool ! Le document peut être récupéré, en HTTPS, depuis un domaine que je gère depuis mon cluster !</p>
<p>Vous aurez remarqué que la requête entre mon poste et l'ingress est en HTTPS, mais depuis mon cluster vers le bucket S3 on reste en HTTP (<code>port.number: 80</code>). Pour faire propre, on met à jour la ressource ingress pour gérer le backend AUSSI en HTTPS :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">piVersion</span><span class="p">:</span><span class="w"> </span><span class="l">networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/backend-protocol</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPS</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">demo-ingress-s3.opsrel.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span>- <span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">          </span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">demo-ingress-s3-upstream</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">            </span><span class="nt">port</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">              </span><span class="nt">number</span><span class="p">:</span><span class="w"> </span><span class="m">443</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span></code></pre></div><p>Et voilà, Avec 2 ressources dans Kubernetes, vous pouvez maintenant récupérer des documents depuis un bucket S3 (ou tout autre document externe) avec vos propres URLs et certificats TLS.</p>
<h2 id="notes">Notes</h2>
<h3 id="références">Références</h3>
<p>Lors de la rédaction de l'article, des confrères de Zenika ont sorti un article sur des possibilités de configuration de l'ingress-controller nginx. N'hésitez pas à aller le lire, c'est <a href="https://dev.to/zenika/kubernetes-nginx-ingress-controller-10-complementary-configurations-for-web-applications-ken">ici</a>.</p>
<h3 id="civo">CIVO</h3>
<p>En tant que &quot;developer advocate&quot; <a href="https://www.civo.com/">Civo</a>, je les remercie de me permettre d'utiliser leurs produits, notamment leur offre de service de cluster k8s managé, utilisée pour cet article.</p>
<p><img alt="Logo CIVO" class="zoomable" decoding="async" loading="lazy" src="/2023/10/2023-10-16-civo-logo.webp"></p>]]></content:encoded></item><item><title>Tour d'horizon du Blinky by Enix</title><link>https://www.vrchr.fr/posts/2023/06/12/blinky/</link><pubDate>Mon, 12 Jun 2023 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2023/06/12/blinky/</guid><description>Tour d&amp;#39;horizon de la solution blinky proposée par Enix</description><content:encoded><![CDATA[<p>En début d'année 2023, alors que je sympathisais avec les équipes orgas du KCD, et notamment les gens d'Enix, est passé dans ma timeline twitter un programme de test &quot;beta&quot; pour un nouveau produit physique, aka le &quot;blinky&quot;. Friand des participations aux beta-tests (j'ai eu accès à la certification Prometheus ainsi), je me suis inscrit.</p>
<p>Quelques temps plus tard, je recevais un mail m'indiquant que j'étais sélectionné, super ! Ce programme permet à quelques heureux de jouer avec le produit, et remonter les usages / idées / bugs à l'équipe, pour qu'ils puissent trouver un business-model adéquat avec le produit, enfin j'imagine.</p>
<p>Je vais donc recevoir le précieux, et pouvoir jouer avec !! Une soirée pour les privilégiés sur Paris a même lieu avec remise en main propre. Pour les autres, comme moi, ce sera livré par voie postale.</p>
<h2 id="blinky">Blinky</h2>
<p><img alt="Blinky emballé" class="zoomable" decoding="async" loading="lazy" src="/2023/06/2023-06-12-blinky-01.webp"></p>
<p>Dans le sachet, on a droit à :</p>
<ul>
<li>Le blinky, of course</li>
<li>Une mini-doc, pour les chanceux ;)</li>
<li>Quelques sucreries !</li>
</ul>
<p>Une fois déballé, on a un petit cercle en plastique de 8 cm de diamètre sur 3 cm de hauteur.
<img alt="Blinky déballé" class="zoomable" decoding="async" loading="lazy" src="/2023/06/2023-06-12-blinky-02.webp"></p>
<p>Ok, je vous parle de l'objet, mais il sert à quoi concrètement ??</p>
<p>Et bien il s'agit d'un mini gyrophare connecté à une appli web (en mode SaaS), qui permet de clignoter sur plusieurs événements, notamment des alertes Prometheus, des actions GitLab, ou même des choses plus génériques, nous y reviendrons dans la partie &quot;Application&quot;.</p>
<h2 id="côté-hardware">Côté hardware</h2>
<p>Alors je ne suis pas du tout expert en la matière, et ça ne m'intéresse pas plus que cela... Tout ce que je peux vous dire c'est que cela fonctionne avec un ESP32-WROOM-32E de chez ESPRESSIF. Je vous laisse lire le thread de <a href="https://twitter.com/zwindler/status/1667156444297216001">@zwindler</a> sur le sujet !</p>
<p>Niveau connectique, il est alimenté par USB-C, avec une alimentation externe (non fournie). Aucune idée de sa consommation.</p>
<p>Ce qui m'intéresse c'est le produit &quot;fini&quot;, et ce que je peux en faire. On passe alors à l'appli !</p>
<h2 id="côté-software">Côté software</h2>
<p>La mini-doc nous invite à aller sur <a href="https://app.getblinky.io/">https://app.getblinky.io/</a>, pour s'enregistrer et configurer l'appareil.</p>
<p>Après avoir entré son mail, on active notre compte, puis on crée une organisation.
<img alt="Application Organization" class="zoomable" decoding="async" loading="lazy" src="/2023/06/2023-06-12-blinky-app-orga.webp"></p>
<h3 id="installation">Installation</h3>
<p>On va maintenant lier notre blinky à l'application. Pour se faire, l'appli utilise la fonctionnalité &quot;WebSerial&quot; des navigateurs. Utilisez alors un navigateur comme Chrome.</p>
<p>Lorsque celui-ci est associé, un petit nom aléatoire lui est donné, que vous pourrez changer au besoin. Le mien s'appelle &quot;Welcome Giraffe&quot;.</p>
<p>Une fois associé, on configure le wifi sur l'objet pour que celui-ci puisse ensuite fonctionner de manière autonome et commiquer avec l'appli web.
<img alt="Blinky Wifi" class="zoomable" decoding="async" loading="lazy" src="/2023/06/2023-06-1-blinky-config-wifi.webp"></p>
<p>Lorsque celui-ci est OK, une petite LED verte sur le boitier montre qu'il est UP.</p>
<h3 id="configuration">Configuration</h3>
<p>Maintenant qu'on a un blinky connecté, que peut-on en faire ? On peut l'interfacer de 3 manières :</p>
<ol>
<li>Via une configuration Prometheus &amp; Alertmanager</li>
<li>Via des hooks GitLab</li>
<li>Via un webhook générique
<img alt="Blinky Integration" class="zoomable" decoding="async" loading="lazy" src="/2023/06/2023-06-12-blinky-integrations.webp"></li>
</ol>
<h4 id="prometheus">Prometheus</h4>
<p>Dans un premier temps, on va regarder ce qui est fait avec Prometheus.</p>
<p>On définit un nom (ici  <code>prometheus-1</code> , super inspiré), puis une réaction, qui va définir un pattern d'affichage, un intervalle mini entre 2 alertes. On va lier notre blinky à cette alerte (dans le cas où l'on en a plusieurs), puis il suffira de copier la configuration d'alertmanager sur notre application en production.</p>
<p>On peut définir des conditions supplémentaires sur les labels, afin d'éviter de surcharger la configuration d'Alertmanager à la source.
<img alt="Blinky Prometheus" class="zoomable" decoding="async" loading="lazy" src="/2023/06/2023-06-12-blinky-prom.webp"></p>
<h4 id="gitlab">GitLab</h4>
<p>De la même manière, on peut créer des événements sur actions GitLab : sur du push, des issues ou pipelines CI/CD.</p>
<p>On défini un pattern à afficher, les blinkies associés, et il suffira ensuite de copier la configuration des hooks dans votre projet GitLab !
<img alt="Blinky GitLab" class="zoomable" decoding="async" loading="lazy" src="/2023/06/2023-06-12-blinky-gitlab.webp"></p>
<h4 id="webhook">Webhook</h4>
<p>Enfin, il est possible de créer un webhook tout simple, qu'on pourra même déclencher via une simple commande <code>curl</code>. Et il est aussi possible de tester directement depuis l'interface web.
<img alt="Blinky Webhook" class="zoomable" decoding="async" loading="lazy" src="/2023/06/2023-06-12-blinky-webhook.webp"></p>
<h3 id="les-patterns">Les patterns</h3>
<p>Je vous ai expliqué comment générer un événement, mais pas comment définir la couleur ou schéma qui permettra au blinky de bien clignoter... Et bien cela se fait avec les patterns !</p>
<p>Il s'agit de séquences qui permettront aux leds de prendre une couleur particulière, pendant un certain laps de temps, et ainsi pouvoir faire clignoter e blinky, faire un chenillard, etc..</p>
<p>Par défaut 8 patterns sont définis, dont le &quot;rainbow&quot; ci-dessous.
<img alt="Rainbow Pattern" class="zoomable" decoding="async" loading="lazy" src="/2023/06/2023-06-12-blinky-rainbow.webp"></p>
<p>Pour en créer un nouveau, c'est SUPER simple, l'interface étant très intuitive.</p>
<p>Ci-après un exemple de rose, pour faire plaisir à ma fille (oui c'est très cliché, mais ça lui plait) :
<img alt="Pink Pattern" class="zoomable" decoding="async" loading="lazy" src="/2023/06/2023-06-12-blinky-pink.webp"></p>
<h2 id="utilisation--feedback">Utilisation &amp; feedback</h2>
<p>Maintenant qu'on a définit des patterns, qu'on les a associés à des intégrations Prometheus, GitLab ou autre, que fait-on pour que ça marche ? Et bien rien, juste s'assurer que le blinky est alimenté, et qu'il s'allume de temps en temps (pas trop souvent non plus, surtout sur déclenchement d'alertes).</p>
<p>Si l'on retourne sur le menu du blinky, on peut voir les derniers messages envoyés, avec différents status : Played, Failed, Pending... Ce serait drole de pouvoir monitorer ces events sur un autre blinky #inception :)
<img alt="Blinky messages" class="zoomable" decoding="async" loading="lazy" src="/2023/06/2023-06-12-blinky-messages.webp"></p>
<p>L'application web est super simple, j'ai eu un blinky fonctionnel en 2 temps 3 mouvements. Et si jamais on s'en sort pas, on peut toujours aller sur le canal discord du projet pour échanger.</p>
<p>Sur le matériel lui-même, ça fait le boulot, juste j'aurais aimé connaitre dans quel sens se met l'objet, suivant les patterns qu'on définit ça peut se jouer à l'envers ;) Et je sais pas aussi si dans un environnement &quot;pro&quot;, une prise Ethernet en PoE serait la bienvenue ?</p>
<p>Enfin bravo à l'équipe, c'est ludique, fun, j'espère que ce petit projet rencontrera un petit succès !
<img alt="Blinky sur le bureau" class="zoomable" decoding="async" loading="lazy" src="/2023/06/2023-06-12-blinky-03.webp"></p>
<h3 id="notes">Notes</h3>
<p>Difficile de maintenir un blog tech à jour, même si celui-ci est tout jeune, ce n'est jamais évident de prendre le temps pour mettre noir sur blanc les idées et articles que l'on souhaite partager.</p>
<p>Malgré un backlog qui se remplit doucement, la &quot;peur de la page blanche&quot; accentue le fait de ne pas écrire autant que je le souhaiterais. Et je ne parle pas non plus des priorités pro et perso ;)</p>
<p>Mais aujourd'hui je me force un peu pour vous parler d'un projet qui change un tout petit peu des REX de confs ou how-tos, j'espère que cela vous a plu !</p>]]></content:encoded></item><item><title>Notes Kubernetes Community Days France 2023</title><link>https://www.vrchr.fr/posts/2023/03/07/kcdfrance/</link><pubDate>Tue, 07 Mar 2023 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2023/03/07/kcdfrance/</guid><description>Retour sur la conférence Kubernetes Community Days France 2023.</description><content:encoded><![CDATA[<h2 id="kubernetes-community-days-france-2023">Kubernetes Community Days France 2023</h2>
<p>Début Mars 2023 s'est tenu le premier KCD France, événement communautaire autour de Kubernetes et l'environnement Cloud Natif.</p>
<p>Etant très friand de ces sujets ces derniers temps, il était hors de question que je loupe cela !</p>
<p>De plus, j'ai eu le privilègre d'être sélectionné pour 2 sujets :</p>
<ul>
<li>Comment bien foirer sa certification CK{A,S}</li>
<li>Table Ronde Communautaire - Comment faciliter les contributions open source et mieux les évangéliser</li>
</ul>
<p>Malgré un contexte de société difficile en cette période (réforme des retraites), les bénévoles organisateurs ont tout fait pour maintenir l'événement, et donc en tant que speaker c'était tout à fait normal de supporter leur choix, et aller dans leur sens. Alors à moi Paris pour encore quelques jours !</p>
<h2 id="avant-goût-meetup-paris-open-source-data-infrastructure">Avant-goût (Meetup Paris Open Source Data Infrastructure)</h2>
<p>Initialement, j'avais prévu un aller-retour dans la journée du mardi, mes 2 sessions étant prévues l'après-midi. Cependant, ce jour étant annoncé comme journée de grève nationale, j'ai étendu mon déplacement, arrivant sur Paris le lundi soir pour repartir le mercredi midi. N'ayant alors rien de prévu la veille de l'événement, et par le hasard du calendrier un meetup organisé par <a href="https://twitter.com/sebi2706">Sébastien Blanc</a> d'Aiven sur l'infra, la data &amp; l'Open Source était prévu dans les locaux d'OVHcloud !</p>
<p>J'ai donc présenté mon rex sur Velero (outil de sauvegarde d'applications sous Kubernetes), à côté d'autres speakers (<a href="https://twitter.com/aurelievache">Aurélie Vache</a> &amp; <a href="https://twitter.com/LostInBrittany">Horacio Gonzàlez</a> d'OVHcloud, <a href="https://twitter.com/ArthurBusser">Arthur Busser</a> de chez Pigment).</p>
<p>Soirée en toute intimité, donnant un avant-goût de LA journée KCD !</p>
<h2 id="le-jour-j--la-conférence-ambiance-environnement-etc">Le Jour J : la conférence, ambiance, environnement, etc.</h2>
<p>Après pas mal de déboires, le KCD France a eu ENFIN lieu en physique, avec une équipe organisatrice remontée à bloc ! Je ne vais pas faire de redite, mais le parcours pour organiser cet événement a été semé d'embuches, et les bénévoles s'en sont finalement très bien sortis. Bravo à eux !</p>
<p>Concernant le lieu, la conférence se déroulait au Centre Pompidou, en plein centre de Paris. L'espace était grand, 3 salles &quot;Bleu&quot;, &quot;Blanc&quot; et &quot;Rouge&quot; pour les sessions, un espace stands sponsors &amp; partenaires, et un espace &quot;Communautaire&quot; sur une mezzannine, qui aurait mérité un peu plus de visibilité. On avait même droit à une retransmission sur grand écran dans le hall pour les malheureux qui n'avaient plus de place.</p>
<p>Après un peu d'attente pour entrer (la gestion des badges est à noter dans les axes d'amélioration), tout le reste de la journée s'est déroulé sans accrocs !</p>
<p>Toujours content de croiser les personnes avec qui j'échange sur le net, des anciens &amp; actuels collègues, et même des amis de Lycée. Mention spéciale à <a href="https://www.linkedin.com/in/aur%C3%A9lien-violet-bb97445/">Aurélien Violet</a> avec qui j'ai pas mal échangé avant l'événement et que j'ai eu l'occasion de croiser en vrai.</p>
<p><img alt="Un Rémi content devant le KCD" class="zoomable" decoding="async" loading="lazy" src="/2023/03/2023-03-07-kcdfr-rve.webp"></p>
<p>Place maintenant aux sessions auxquelles j'ai pu assister !</p>
<h2 id="keynote-douverture">Keynote d'ouverture</h2>
<p>Arrivé avec un peu de retard sur la keynote (ça commence bien), j'ai pris au vol les échanges entre <a href="https://twitter.com/solomonstre">Solomon Hykes</a> et <a href="https://twitter.com/jpetazzo">Jérôme Petazzoni</a> sur Docker, et le nouveau projet Dagger.io.</p>
<p>La vidéo de la keynote est disponible <a href="https://youtu.be/OKIehz7p4ug">ici</a>.</p>
<h3 id="echanges-avec-solomon-hykes--daggerio">Echanges avec Solomon Hykes &amp; Dagger.io</h3>
<p>Ce projet semble très prometteur, car tout l'éco-système de la CI/CD se cherche encore (parlez-en à l'équipe <a href="https://twitter.com/r2devops_io">R2Devops</a> !), et Dagger essaie de rationnaliser tout cela (fini les commits &quot;update CI/CD&quot; entre autre). Dagger vient notamment de &quot;DAG&quot; pour &quot;Directed Acyclic Graph&quot;.</p>
<p><img alt="Dagger" class="zoomable" decoding="async" loading="lazy" src="/2023/03/2023-03-07-kcdfr-dagger.webp"></p>
<p>Nous avions fait un workshop en interne sur le sujet, mais l'outil évolue très vite qu'il faudra retester.</p>
<h3 id="red-hat-openshift">Red Hat Openshift</h3>
<p>Red Hat est une entreprise que j'apprécie beaucoup, pour toutes les contributions qu'elle peut faire (<em>ndlr: je suis RHCE, j'espère bientôt RHCA</em>).</p>
<p>Elle nous présente alors OpenShift, avec une définition super simple : Kubernetes avec beaucoup d'amour ;)</p>
<p><img alt="Openshift" class="zoomable" decoding="async" loading="lazy" src="/2023/03/2023-03-07-kcdfr-openshift.webp"></p>
<h3 id="rex-de-leducation-nationale">REX de l'Education Nationale</h3>
<p>L'Education Nationale nous a fait un retour de l'utilisation de Kubernetes, notament sur la gestion des copies du Baccalauréat avec le projet SANTORIN.</p>
<p>L'équipe n'est pas grand,e mais les challenges le sont ! Et avec beaucoup d'outils (et de volonté), ils proposent une belle usine logicielle pour faire tourner tout cela, avec un socle Kubernetes bien sûr.</p>
<h3 id="talks-sponsors">Talks Sponsors</h3>
<p>Les sponsors Platinum ont présentés leurs produits, mais c'est Scaleway qui saura se faire remarquer avec quelques minutes de rafraichissement sur la prononciation de <code>kubectl</code>.</p>
<h2 id="la-plus-value-dun-portail-développeur-chez-back-market">La plus-value d'un portail développeur chez Back Market</h2>
<p>Place maintenant aux sessions techniques / rex ! On commence par <a href="https://www.linkedin.com/in/farhatsami">Sami Farhat</a> de Black Market qui nous explique comment ils en sont arrivé à mettre en place un &quot;Developer Portal&quot; avec l'outil Backstage.</p>
<p>L'idée ici est d'avoir un portail 'self-service' pour faciliter la vie des devs, qu'ils soient le plus autonomes pour travailler et gérer leurs environnements à la demande.</p>
<p>Si l'idée est top (on va en entendre parler avec les platform engineers), il faut cependant considérer la mise en place d'un tel outil comme un produit à part entière, qu'il soit fiable pour que l'adoption soit complète, et que ce ne soit pas un nième outil d'automatisation ni fait ni à faire.</p>
<p><img alt="backstage" class="zoomable" decoding="async" loading="lazy" src="/2023/03/2023-03-07-kcdfr-backstage.webp"></p>
<p>La vidéo est disponible <a href="https://youtu.be/2XghfHsbRtw">ici</a>.</p>
<h3 id="commentaires">Commentaires</h3>
<blockquote>
<p>La notion d'IDP est très intéressante, à suivre de prêt. J'aurais bien aimé une &quot;démo&quot; de leur plateforme pour mieux me rendre compte du travail fourni.
Etrange aussi, la conf été en Anglais, alors que le speaker maitrisait bien le Français pendant les questions/réponses.</p>
</blockquote>
<h2 id="sécurisez-votre-software-supply-chain-avec-slsa-sigstore-et-kyverno">Sécurisez votre software supply chain avec SLSA, Sigstore et Kyverno</h2>
<p>Moi qui pensait assister à une conf sur une extension des SLAs, j'ai été surpris, mais pas dépaysé car la sécurité reste un enjeu majeur dans les environnements Cloud Native.</p>
<p><a href="https://www.linkedin.com/in/abdennebi/">Mohamed Abdennebi</a> de Sfeir nous présente alors SLSA, qui est la pour répondre au besoin d'assurer l'intégrité de la supply chain. Plusieurs niveaux d'exigence existe, avec diverses solutions à mettre en oeuvre, notamment Sigstore &amp; Kyverno.</p>
<p>Les différents outils de Sigstore nous ont été présenté (Cosign le plus connu, mais aussi Fulcio et Rekor), avec une démo de chaque étape de la sécurisation de la Supply Chain.</p>
<p>Comme à chaque fois quand on parle sécurité, beaucoup d'actions à faire pour s'assurer de la conformité des applications de bout en bout, c'est un sujet où l'on ne risque pas de s'ennuyer !</p>
<p><img alt="SLSA" class="zoomable" decoding="async" loading="lazy" src="/2023/03/2023-03-07-kcdfr-slsa.webp"></p>
<p>La vidéo est disponible <a href="https://youtu.be/cWpC96J05gI">ici</a>.</p>
<h3 id="commentaires-1">Commentaires</h3>
<blockquote>
<p>Présentation claire de Mohamed, sur l'ensemble des composants à mettre en place pour être SLSA compliant.</p>
</blockquote>
<h2 id="shift-happen--sécurité-de-vos-environnements-cloud-native">Shift Happen : Sécurité de vos environnements Cloud Native</h2>
<p>Un lightning talk de <a href="https://www.linkedin.com/in/smontri/">Stéphane Montri</a> de chez Palo Alto sur la sécurité de bout en bout d'un projet.. ça me rappelle le talk juste au dessus tiens ! ;)</p>
<p>On ne le dit jamais assez, le niveau de sécurité d'un projet est conditionné par le maillon le plus faible de la chaîne... Et cette chaîne est longue :</p>
<ul>
<li>IDE</li>
<li>Gestionnaire de code source</li>
<li>CI/CD</li>
<li>Registre d'image</li>
<li>Admission Controller</li>
<li>Applications tierces</li>
<li>etc.</li>
</ul>
<p>Bref, encore et toujours du boulot pour la sécu !</p>
<p><img alt="SLSA" class="zoomable" decoding="async" loading="lazy" src="/2023/03/2023-03-07-kcdfr-prisma.webp"></p>
<p>La vidéo est disponible <a href="https://youtu.be/x39zn0dNf88">ici</a>.</p>
<h2 id="comment-bien-foirer-sa-certification-ckas">Comment bien foirer sa certification CK{A,S}</h2>
<p>A moi de jouer ! Suite à 2 passages de certifications foireux, j'ai décidé l'an dernier d'en faire un article de blog, qui a été transformé en mini-conf au Camping des Speakers 2021, pour au final être présenté ici-même ! Je n'en revenais pas !</p>
<p>Pendant 10 minutes, j'ai partagé quelques non-astuces pour louper sa certif, histoire de dédramatiser un peu la situation, montrant aux plus jeunes ou moins expérimentés que tout le monde peut se planter.</p>
<p>Je me suis REGALE à faire cette présentation, et le public a été super réceptif (merci d'ailleurs à celle qui avait un rire très communicatif). J'ai eu beaucoup de retours positifs à la fin de la session, comme quoi sur un malentendu, un sujet que vous pensez anodin peut aider d'autres !</p>
<p>La vidéo est disponible <a href="https://youtu.be/NHHlKoEdT2M">ici</a>.</p>
<h2 id="comment-faciliter-les-contributions-open-source-et-mieux-les-évangéliser">Comment faciliter les contributions open source et mieux les évangéliser</h2>
<p>Pas le temps de se reposer, j'enchaine avec une table ronde sur les contributions Open Source.</p>
<p>J'avais fait une présentation à l'Open Source Experience 2020 à ce propos, et c'est en partie pour cela que Denis Germain aka <a href="https://blog.zwindler.fr/">Zwindler</a>, l'orga des tables rondes, m'a proposé d'y participer. Toujours à relever les nouveaux défis, j'ai accepté l'invitation !</p>
<p>Pendant 30 minutes, nous avons échangé avec <a href="https://twitter.com/c_chaudier">Christophe Chaudier</a> des Compagnons du DevOps et <a href="https://twitter.com/ZinebBendhiba">Zineb Bendhiba</a> de Red Hat sur comment faciliter la contribution.</p>
<p>La table ronde, animée par <a href="https://twitter.com/leiluspocus">Laïla Atrmouh</a> de Ladies Of Code, était très enrichissante car l'on a pu partager nos divers points de vue : en tant que mainteneur de projets, &quot;évangéliste&quot;, ou simple contributeur. Nous sommes tous d'accord sur un point : contribuez, ça ne vous apportera que du bon !</p>
<p>Mention spéciale à Zineb, qui parle de syndrôme de l'imposteur alors qu'elle est la plus légitime sur le plateau.</p>
<p>La vidéo est disponible <a href="https://youtu.be/XalAQM1-YMg">ici</a>.</p>
<h2 id="comment-rendre-les-secrets-kubernetes-vraiment-secrets">Comment rendre les secrets Kubernetes vraiment… secrets?</h2>
<p>Dernier talk de la journée, <a href="https://twitter.com/louhdetech">Julie Hourcade</a> de chez Formance souhaitait nous révéler un secret... sur les secrets Kubernetes !</p>
<p>Comment les rendre secrets ? Car par défaut, on ne peut pas dire qu'un encodage base64 soit très sécurisé...</p>
<p>Après avoir fait le tour des solutions possibles, elle nous montre à chaque fois ce qui ne va pas dans ces solutions, et la complexité que cela peut amener.</p>
<p>Cependant, elle nous préconise les solutions les &quot;moins pire&quot;, tant sur la partie sécu que la partie complexité, avec une conclusion qui est toujours énervante, mais réaliste : &quot;ça dépend&quot; ;)</p>
<p><img alt="Secrets" class="zoomable" decoding="async" loading="lazy" src="/2023/03/2023-03-07-kcdfr-secrets.webp"></p>
<p>La vidéo est disponible <a href="https://youtu.be/RQqmym_34WY">ici</a>.</p>
<h3 id="commentaires-2">Commentaires</h3>
<blockquote>
<p>Belle présentation des solutions existantes, avec leurs avantages et inconvénients.
Sur la forme, les messages passaient bien, avec une pointe d'humour piquant. Le stress a fait que Julie est allé super vite, le talk a été écourté, mais laissant du coup beaucoup de place aux questions / réponses.</p>
</blockquote>
<h2 id="goodies">Goodies</h2>
<p>Enfin, une conf tech sans goodies ce n'est pas une conf tech. Autant je reste mesuré et ne prends que quelques stickers pour les enfants en tant normal, autant cette fois-ci j'ai craqué.</p>
<p>Et le prix du goodies le plus utile revient à HA-Proxy avec sa batterie solaire, merci !</p>
<p>Je suis dégouté par contre, ayant complètement zappé le stickers bar de l'espace communautaire :(</p>
<p><img alt="goodies" class="zoomable" decoding="async" loading="lazy" src="/2023/03/2023-03-07-kcdfr-goodies.webp"></p>
<h2 id="ils-en-parlent-aussi">Ils en parlent aussi</h2>
<p>Retrouvez deux autres articles de blog à ce sujet :</p>
<ul>
<li><a href="https://blog.antoinemayer.fr/2023/03/10/une-journee-avec-moi-au-kubernetes-community-days/">Une journée avec moi au Kubernetes Community Days</a> par <a href="https://www.linkedin.com/in/antoine-mayer/">Antoine Mayer</a></li>
<li><a href="https://leiluspocus.netlify.app/fr/posts/2023/03/notes-du-kubernetes-community-days-france/">Notes du Kubernetes Community Days France</a> par <a href="https://twitter.com/leiluspocus">Laïla Atrmouh</a></li>
<li><a href="https://blog.zwindler.fr/2023/03/11/ma-premiere-experience-d-orga-kcd-france-2023/">Ma première expérience d'orga - Retours sur KCD France 2023</a> par <a href="https://twitter.com/zwindler">zwindler</a></li>
<li><a href="https://blog.cockpitio.com/cloud/kubernetes-community-days-france-2023/">Cockpit io au Kubernetes Community Days France 2023</a> par l'équipe de cockpit.io</li>
</ul>]]></content:encoded></item><item><title>Notes Very Tech Trip 2023</title><link>https://www.vrchr.fr/posts/2023/02/02/verytechtrip/</link><pubDate>Thu, 02 Feb 2023 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2023/02/02/verytechtrip/</guid><description>Retour sur la conférence Very Tech Trip 2023.</description><content:encoded><![CDATA[<h2 id="very-tech-trip">Very Tech Trip</h2>
<p>Organisée par OVHcloud, cette conférence réunit l'ensemble des acteurs de la tech FR, avec comme sujets tout ce qui touche de prêt ou de loin les solutions que proposent le cloud provider : Cloud Native, Infrastructure, Code, IA, Electronique, bref des sujets techs pour les techs !</p>
<p>Cette conférence se veut orientée tech &amp; communautaire, avec des retours d'expérience plutôt que des speechs partenaires, contrairement à l'OVH Summit.</p>
<p>Very Tech Trip, c'est donc une journée de rex tech à la Cité des Sciences et de l'Industrie à Paris, par les techs, pour les techs, le tout dans une ambiance décontractée et la bonne humeur !</p>
<p>Toutes les vidéos sont disponibles ici : <a href="https://verytechtrip.ovhcloud.com/fr/sessions/">https://verytechtrip.ovhcloud.com/fr/sessions/</a></p>
<h2 id="la-conférence-ambiance-environnement-etc">La conférence, ambiance, environnement, etc.</h2>
<p>Comme dit plus haut, la conférence c'est un jour de rex sur Paris, dans un bel endroit, et c'était la première édition !</p>
<p>Beaucoup de sessions étaient prévues, 12 thèmes, 10 salles (pour des confs, labs, lightning talks), ça fait beaucoup.</p>
<p>Arrivé dès l'ouverture, une marée de t-shirt bleus m'attendaient, OVHcloud était là en force ! J'ai pu alors profiter du calme avant les sessions pour échanger avec les responsables produits Managed Kubernetes, Developer Experience, les DevRels que je commencent à croiser souvent. Etant client OVHcloud, c'était l'occasion pour discuter IRL, remonter les problématiques &quot;terrain&quot;, et en savoir plus sur leur roadmap. Toute l'équipe est très demandeuse de retour d'XP, surtout sur des sujets relativements nouveaux.</p>
<p><img alt="Team DevRels" class="zoomable" decoding="async" loading="lazy" src="/2023/02/2023-02-02-vtt-devrels.webp"></p>
<p>J'ai eu aussi la &quot;faiblesse&quot; de répondre affirmativement quand la veille, lors de la soirée des speakers, on m'a proposé de devenir &quot;sponsor communautaire&quot; et répondre à une interview, j'y reviendrai plus tard.</p>
<p>Ambiance détendue donc, et c'était aussi l'occasion pour moi de rencontrer les personnes que je suis sur les réseaux sociaux, et tout particulièrement la team <a href="https://devobs.p7t.tech/">DevObs</a>, qui aime les débats animés, mais qui n'est pas si obtue en vrai, avec une vraie expertise sur les sujets techs ;)</p>
<p>Place maintenant au sessions auxquelles j'ai pu assister, qui ne sont pas si nombreuses, pour diverses raisons : j'ai été speaker, interviewé, ayant passé pas mal de temps à discuter avec les teams OVHcloud et les autres speakers et techs. Le temps passe vite quand on est en bonne compagnie !</p>
<h2 id="keynote-douverture">Keynote d'ouverture</h2>
<p>Organisateur de la conférence et principal sponsor oblige, Octave Klaba et les divers responsables des branches produits OVHcloud nous présentent chacun leur tour les différentes nouveautés et annoncent en fonction des divers segments.</p>
<p>Ca parle de Public Cloud, Databases, Serveurs dédiés, etc...</p>
<p>Je retiens parmi les annonces 2 choses qui m'intéressent:</p>
<ul>
<li>l'IAM qui va être bientôt dispo</li>
<li>L'offre Managed Kubernetes Service Multi Cloud</li>
</ul>
<p>Maintenant les annonces faites, on attend patiemment les offres GA ;)</p>
<p><img alt="IAM Beta" class="zoomable" decoding="async" loading="lazy" src="/2023/02/2023-02-02-vtt-iam.webp"></p>
<h3 id="commentaires">Commentaires</h3>
<blockquote>
<p>Au delà des annonces qui sont toujours bien à prendre, désolé la team OVHcloud mais si sur le font les informations étaient pertinentes, sur la forme je note des &quot;axes d'amélioration&quot;. Les responsables produits avaient les mêmes mimiques, en surjouant, je pense qu'il faudrait vous rapprocher de vos DevRels pour un coaching speaker tech plus efficace ;)</p>
</blockquote>
<h2 id="velero--sauvegardez-et-restaurez-proprement-vos-applications-kubernetes">Velero : Sauvegardez et Restaurez (proprement) vos applications kubernetes</h2>
<p>De suite après la keynote, j'ai eu le privilège de présenter un REX sur Velero.</p>
<p>La salle était remplie, 100 personnes à m'écouter religieusement comment tenter de bien restaurer des données sur un cluster kubernetes !</p>
<p>Après quelques problèmes techniques niveau HDMI, ça a bien enchainé, et les retours en fin de session est toujours intéressante. Peu de questions mais des partages d'expérience pour compléter mes propos, c'est cool !</p>
<p>C'était la 2ème fois que je donnais cette conf, plus à l'aise sur le sujet, donc de mon côté RAS, ça a bien déroulé !</p>
<h2 id="entrevue">Entrevue</h2>
<p>A peine fini ma conf, j'enchaine sur un entretien mené par William Dubreuil &quot;Communities Experience Manager&quot; chez OVHcloud, qui m'avait proposé la veille de répondre à quelques questions en live.</p>
<p>L'interview assez courte, les sujets tournaient autour de l'événement, OVHcloud et la tech en général. Je ne sais pas encore si les vidéos seront dispo, mais l'exercice était toujours bon à prendre !</p>
<p>On notera une volonté d'OVHcloud d'être plus proche de la communauté tech, on attend de voir ce que cela va donner.</p>
<h2 id="et-si-vous-aussi-vous-construisiez-des-robots-">Et si, vous aussi, vous construisiez des robots ?</h2>
<p>Stéphanie Moallic, front end dev sur les solutions de téléphonie d'OVHcloud, nous présente un truc qui n'a strictement rien à voir avec son job : les robots !</p>
<p>Elle nous partage sa passion, avec tous les différents types de robots qu'elle aime programmer &amp; animer, son parcours robotique l'utilisation qu'elle en a avec les enfants.</p>
<p>Avec quelques démos live à la clé, elle nous présente ses petites bêtes, de la plus basique à la plus complexe, sans accrocs !</p>
<p><img alt="Robots" class="zoomable" decoding="async" loading="lazy" src="/2023/02/2023-02-02-vtt-robots.webp"></p>
<h3 id="commentaires-1">Commentaires</h3>
<blockquote>
<p>Avec &quot;un peu de stress&quot;, on sentait quand même la passion qu'a Stéphanie avec ces petits jouets, et le plaisir qu'elle a à transmettre aux plus jeunes. Bravo!</p>
</blockquote>
<blockquote>
<p>Et même si cela n'est pas mon sujet de prédilection, je me devais d'assister à sa conf pour la supporter (j'ai rencontré Stéphanie au Camping des Speakers, on se refait pas !)</p>
</blockquote>
<h2 id="cest-quoi-un-bastion-et-pourquoi-vous-devriez-en-utiliser-un-">C'est quoi un Bastion et pourquoi vous devriez en utiliser un ?</h2>
<p>Stéphane Lesimple d'OVHcloud nous présente la solution de bastion &quot;The Bastion&quot; made in OVHcloud, produit maison Open Source pour accéder à l'ensemble des serveurs que les admins gèrent.</p>
<p>Après avoir expliqué les différentes méthodes pour se connecter à des serveurs, notamment les problèmes que peuvent engendrer des connexions directes, Stéphane nous explique ce qu'est un bastion, à quoi cela sert, les problèmes qu'il résoud et comment justement a été pensé l'outil d'OVHcloud.</p>
<p>Avec une gestion &quot;de masse&quot; de serveurs, &quot;The Bastion&quot; répond bien au besoin, avec une expérience sysadmin (à défaut d'expérience développeur) la plus efficace possible, pour que l'utilisation de l'outil soit le plus transparent possible, avec une gestion de droits efficaces.</p>
<p><img alt="Robots" class="zoomable" decoding="async" loading="lazy" src="/2023/02/2023-02-02-vtt-bastion.webp"></p>
<h3 id="commentaires-2">Commentaires</h3>
<blockquote>
<p>L'utilisation d'un Bastion est quasi obligatoire quand on doit administrer des machines et gérer/tracer/auditer les accès.. Est-ce que l'outil rempli ce besoin ? Oui.</p>
</blockquote>
<blockquote>
<p>De mon côté, utilisant beaucoup Kubernetes ces temps-ci, j'aurai bien aimé savoir si le coup du HTTPS proxy fonctionne bien pour ce cas d'usage.</p>
</blockquote>
<h2 id="keynote-de-fin">Keynote de fin</h2>
<p>Déjà fini les confs (et oui, le temps passe trop vite quand on est bien entouré), on termine par un échange de questions / réponses par l'équipe dirigeante d'OVHcloud.</p>
<p>Les questions ont été posées au fil de la journée par les participants, et l'équipe de modération en avait sélectionnée quelques-unes.</p>
<p>Parmi les sujets, un m'a particulièrement intéressé, à savoir la participation d'OVHcloud à l'écosystème Open Source : Octave a répondu qu'ils avaient une forte volonté d'y participer, mais que ce n'était pas si simple que cela. Mettre juste du code sur Github ne suffit pas, il faut bien packager les projets, avoir un vrai suivi communautaire, et s'il pouvait il y aurait du personnel OVHcloud dédié à cela. J'espère que cela arrivera un jour !</p>
<h2 id="meet--greet">Meet &amp; Greet</h2>
<p>Après les nombreuses sessions, le fameux Meet &amp; Greet a permis de continuer les discussions autour de quelques verres et buffet.</p>
<p>Le fait d'être à la Cité des Sciences à permis au visiteurs de profiter de quelques expositions pendant la soirée, et c'était très chouette ! (Se retrouver à chanter du Amel Bent avec la team DevOb's &quot;pour la science&quot; est assez improbable).</p>
<p>Avec pour terminer un DJ mettant l'ambiance, la soirée s'est très bien terminée, même si je suis plus team &quot;je reste au bar pour discuter tranquille&quot; avec du son moins fort.</p>
<h2 id="pour-finir">Pour finir</h2>
<p>Première édition réussie !</p>
<p>J'ai beaucoup aimé pouvoir échanger avec la team OVH sans langue de bois, qui était très humble et preneur d'infos pour améliorer ses produits.</p>
<p>Côté &quot;people&quot;, j'ai bien aimé discuter avec les autres speakers, certains que j'ai croisé déjà plusieurs fois, toujours présents avec le même enthousiasme. Et échanger avec ses pairs sur des sujets techs n'a pas de prix.</p>
<p>J'attends aussi la suite pour le côté communautaire d'OVHcloud, que cette première édition ne soit que le début d'une longue série.</p>
<p>Rendez-vous en 2024 j'espère !</p>]]></content:encoded></item><item><title>Quelques Exporters Prometheus pratiques</title><link>https://www.vrchr.fr/posts/2022/12/30/quelques-exporters-prometheus-pratiques/</link><pubDate>Fri, 30 Dec 2022 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2022/12/30/quelques-exporters-prometheus-pratiques/</guid><description>Quelques exporters prometheus pratiques pour administrer un cluster Kubernetes</description><content:encoded><![CDATA[<p>Suite à un mini thread <a href="https://twitter.com/rverchere/status/1606237656865771520">Twitter</a>, je mets par écrit ici quelques lignes sur les exporters Prometheus qui peuvent s'avérer pratiques pour administrer un cluster Kubernetes.</p>
<h2 id="x509-certificate-exporter">X509 Certificate Exporter</h2>
<p>Proposé par la société <a href="https://enix.io/fr/">Enix</a>, l'exporter <a href="https://github.com/enix/x509-certificate-exporter">x509-certificate-exporter</a> permet de suivre les certificats x509 de vos applications, et être notifié sur l'expiration de ceux-ci.</p>
<p>J'utilise en général <a href="https://cert-manager.io/">cert-manager</a> et <a href="https://letsencrypt.org/">Let's Encrypt</a>, qui permet d'automatiser la création et renouvellement de certificats, surtout au niveau ingress et HTTPS, mais cela n'empêche pas d'avoir des alertes sur le renouvellement des certificats :</p>
<ul>
<li>Le challenge Let's Encrypt qui échoue pour diverses raisons (principalement des erreurs réseaux).</li>
<li>Un secret Kubernetes qui contient un ancien certificat qui n'est pas nettoyé.</li>
</ul>
<p>Un dashboard est même fourni avec, que vous pourrez modifier selon vos envies :</p>
<p><img alt="x509 dashboard" class="zoomable" decoding="async" loading="lazy" src="https://raw.githubusercontent.com/enix/x509-certificate-exporter/main/docs/grafana-dashboard.jpg"></p>
<h2 id="helm-exporter">Helm Exporter</h2>
<p>J'utilise beaucoup de charts Helm pour déployer les applications sur Kubernetes, surtout les briques d'infrastructure (prometheus, nginx, cert-manager, velero, etc.).
G
Je souhaitais alors pouvoir suivre les versions de charts Helm sur l'ensemble des clusters administrés, et je suis tombé sur 2 outils:</p>
<ol>
<li>
<p><a href="https://nova.docs.fairwinds.com/">Nova</a> de la société Fairwinds, un outil en CLI qui permet de lister les charts dépréciés. L'outil est pratique, mais difficile d'avoir une visualisation globale, ainsi qu'un tableau de bord</p>
</li>
<li>
<p>L'exporter <a href="https://github.com/sstarcher/helm-exporter">helm-exporter</a> qui va générer des métriques Prometheus pour les charts installés au sein du cluster, avec les versions également dépréciées.</p>
</li>
</ol>
<p>Je me suis arrêté sur ce deuxième choix, car il est ainsi facile d'avoir un dashboard dans Grafana.</p>
<p>La configuration est un tout petit peu &quot;pénible&quot;, dans le sens où il faut définir la source de chaque chart (le tout est expliqué sur le dépôt GitHub).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">helmRegistries</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">override</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">      </span>- <span class="nt">registry</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">          </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://charts.jetstack.io&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">        </span><span class="nt">charts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">          </span>- <span class="s2">&#34;cert-manager&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span><span class="nt">allowAllReleases</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span>- <span class="nt">registry</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">          </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://charts.gitlab.io&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="nt">charts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">          </span>- <span class="s2">&#34;gitlab-runner&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">        </span><span class="nt">allowAllReleases</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span>- <span class="nt">registry</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">          </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://shanestarcher.com/helm-charts&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span><span class="nt">charts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">          </span>- <span class="s2">&#34;helm-exporter&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">        </span><span class="nt">allowAllReleases</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">      </span>- <span class="nt">registry</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">          </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://kubernetes.github.io/ingress-nginx&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">        </span><span class="nt">charts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">          </span>- <span class="s2">&#34;ingress-nginx&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">        </span><span class="nt">allowAllReleases</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span></code></pre></div><p>Ainsi, on peut voir que mes clusters ont bien le chart précédemment installé à jour ;)</p>
<p><img alt="helm exporter" class="zoomable" decoding="async" loading="lazy" src="/2022/12/2022-12-30-prom-exporters-helm.webp"></p>
<h2 id="blackbox-exporter">Blackbox Exporter</h2>
<p>On revient aux fondamentaux, le <a href="https://github.com/prometheus/blackbox_exporter">blackbox_exporter</a> reste un exporter très utile lorsqu'on doit monitorer des sites ou équipements distants, ne répondant qu'en HTTP(S), DNS, TCP.</p>
<p>Je l'utilise surtout pour avoir les temps de réponse des sites déployés, depuis un site tiers.</p>
<p><img alt="website metrics" class="zoomable" decoding="async" loading="lazy" src="/2022/12/2022-12-30-prom-exporters-blackbox.webp"></p>
<h2 id="harbor-exporter">Harbor Exporter</h2>
<p>Un petit dernier, quand on est en environnement conteneurs et qu'on gère une registry (Harbor notamment), on peut suivre son état avec celui-ci. Il existe l'exporter <a href="https://github.com/c4po/harbor_exporter">harbor_exporter</a>.</p>
<p>Je l'utilise assez peu, mais cela me permet de voir quels projets consomment le plus de place quand il faut y faire du ménage (il est si facile de créer des images Docker...).</p>
<p><img alt="harbor exporter" class="zoomable" decoding="async" loading="lazy" src="https://raw.githubusercontent.com/c4po/harbor_exporter/master/grafana/screenshot.png"></p>
<h2 id="prometheus-ms-teams">Prometheus MS Teams</h2>
<p>Ce n'est pas un exporter, mais je note quand même l'outil <a href="https://github.com/prometheus-msteams/prometheus-msteams">prometheus-msteams</a>, qui est un serveur web léger en Go qui permet de récupérer des alertes générées via webhook pour les transmettre à un canal MS Teams.</p>
<p><img alt="promteams" class="zoomable" decoding="async" loading="lazy" src="https://raw.githubusercontent.com/prometheus-msteams/prometheus-msteams/master/docs/promteams.png"></p>
<p>Testé chez moi avec succès !</p>
<h2 id="cabourotte--appclacks">Cabourotte &amp; Appclacks</h2>
<p>Si cela nous vous suffit pas, allez voir aussi ce que fait <a href="https://www.mcorbin.fr/">@mcorbin</a>, avec <a href="https://www.doc.appclacks.com/">Appclacks</a> et sa sonde health-check <a href="https://www.cabourotte.appclacks.com/">cabourotte</a>. Projet prometteur !</p>
<p>Les <em>probes</em> se pilotent via API au travers d'une CLI, et il existe même un provider <a href="https://github.com/appclacks/terraform-provider-appclacks">Terraform</a>. On peut soit utiliser les sondes fournies, soit ses propres sondes avec cabourotte, qui ira récupérer sa config auprès du serveur central.</p>
<h2 id="dautres-ressources">D'autres ressources</h2>
<p>Dernier petit lien, si vous devez administrer un serveur Mastodon (rien à voir avec Kubernetes, certes), allez voir <a href="https://medium.com/@dma42/observing-hachyderms-24e0f35529a5">ici</a>.</p>]]></content:encoded></item><item><title>Notes DevOps D-Day 2022</title><link>https://www.vrchr.fr/posts/2022/12/01/devops-dday/</link><pubDate>Thu, 01 Dec 2022 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2022/12/01/devops-dday/</guid><description>Retour sur la conférence DevOps D-Day 2022.</description><content:encoded><![CDATA[<h2 id="devops-d-day">DevOps D-Day</h2>
<p>Comme presque chaque année depuis 2015 (pas merci la COVID), la conférence DevOps D-Day se tient à Marseille, dans le fameux stade Orange Vélodrome.</p>
<p>C'est un événement à ne pas manquer lorsqu'on est en région !</p>
<p>Pour moi, cette conférence est assez particulière, car c'est un peu grâce à cet événement que j'ai découvert et me suis lancé dans l'aventure &quot;DevOps&quot;, &quot;Cloud Native&quot;, &quot;Conteneurs&quot; and co. J'ai expliqué tout cela d'ailleurs sur un <a href="https://twitter.com/rverchere/status/1594631932926889984">thread Twitter</a>.</p>
<p>De plus, cette année, je suis speaker, en présentant un talk sur <a href="https://presentations.verchere.fr/Backup_Velero_DevOps_Dday_2022/">Velero</a>, outil de sauvegarde en environnement Kubernetes.</p>
<p>Enfin, vous verrez que mes notes sont assez concises. Sortant d'une opération au visage (celles &amp; ceux qui m'ont croisé l'ont remarqué), j'ai du me préserver pour être en forme pour ma session, avec quelques pauses repos. Je n'ai d'ailleurs pas &quot;live-tweeté&quot; pour la même raison. Mais pas d'inquiétude, je vais bien ;)</p>
<p>Passons maintenant au debrief !</p>
<p><img alt="Prêt à rentrer dans l'arène !" class="zoomable" decoding="async" loading="lazy" src="/2022/12/2022-12-01-stade.webp"></p>
<h2 id="la-conférence-ambiance-environnement-etc">La conférence, ambiance, environnement, etc.</h2>
<p>Le stade Orange Vélodrome fait toujours son petit effet quand on y est ! Les nouveaux participants se prennent en photo devant le stade, et on croise beaucoup d'anciens collègues toujours aussi ravis d'être là !</p>
<p>Cette année, les conférences sont réparties sur 5 salles, les sessions étant organisées par catégories de sujets : cela permet aux participants de mieux suivre des &quot;tracks&quot; s'ils le veulent.</p>
<p>À l'étage principal, on y retrouve les partenaires avec les stands, ainsi que le coin repas.</p>
<p>On m'a fait la remarque qu'il y avait moins de personnes que l'an dernier. Au contraire, j'ai eu l'information que le public était plus nombreux (à confirmer par les organisateurs), mais le fait d'avoir 1 salle en plus et plus d'espace au niveau des stands donnait cette impression.</p>
<p>A titre personnel, je n'aime pas être les uns sur les autres, donc si l'événement permet à plus de participants de venir ET de pouvoir circuler librement, je dis banco !</p>
<p>Place maintenant aux conférences. J'ai essayé de suivre des sujets que je connais ou ne maitrise pas, afin de découvrir toujours de nouvelles choses.</p>
<h2 id="unfix-lanti-framework-dagilité-à-léchelle-">UnFIX, l’anti framework d’agilité à l’échelle ?</h2>
<p><a href="https://twitter.com/crochas">Clément Rochas</a> et <a href="https://twitter.com/thomasclavier">Thomas Clavier</a> nous présente le framework &quot;unFIX&quot;, qui permet de modéliser SON entreprise, avec plein de carrés, ronds, rectangles de toutes les couleurs !</p>
<p>Pour les agilistes, cela permet d'avoir une &quot;librairie&quot; commune pour s'organiser et bien communiquer, en utilisant son propre vocabulaire.</p>
<p>Après une présentation de chaque composant / team, le duo enchaine les questions / réponses sur pourquoi unFIX, ce que cela peut améliorer, comment le mettre en œuvre.</p>
<p>Quelques points intéressants :</p>
<ul>
<li>Les managers se retrouvent en équipe, et doivent donc fonctionner en équipe</li>
<li>La notion de forum permet d'avoir des communautés inter-équipes, mais où l'on ne doit pas passer tout son temps</li>
<li>On sait que tout n'est pas figé, et donc il est possible de faire des équipes temporaires, avec certaines règles (fréquence, stabilité, etc.)</li>
</ul>
<p>UnFIX est vraiment récent, et n'a été &quot;découvert&quot; qu'en début d'année 2022.</p>
<p>Un <a href="https://aqoba.fr/posts/20221129-%C3%A0-la-d%C3%A9couverte-dunfix/">blog post</a> d'Aqoba est disponible pour celles &amp; ceux qui veulent en savoir plus.</p>
<p><img alt="UnFIX" class="zoomable" decoding="async" loading="lazy" src="/2022/12/2022-12-01-unfix.webp"></p>
<h3 id="commentaires">Commentaires</h3>
<blockquote>
<p>Très bon démarrage de conf, j'ai noté quelques bouquins à lire (encore), la salle était pleine, le sujet intéressant. Ce n'est pas mon rôle de définir ce genre d'organisation, mais c'est toujours bien de savoir que cela existe le jour où une réorg s'annonce ;)</p>
</blockquote>
<h2 id="tips-pour-combattre-le-syndrome-de-limposteur">Tips pour combattre le syndrome de l'imposteur</h2>
<p>Suite à un <a href="https://twitter.com/aurelievache/status/1598432322365300738?s=20&amp;t=d6REBmUcmbh_miTze2I_aQ">aléa du direct</a> ;) j'ai été aux premières loges pour la présentation d'<a href="https://twitter.com/aurelievache">Aurélie Vache</a> sur le syndrome de l'imposteur, et comment transformer cette &quot;faiblesse&quot; en force.</p>
<p>En salle plénière, Aurélie nous explique ce qu'est ce syndrome, comment le combattre, et même comment en tirer parti.</p>
<p>Le mot &quot;humilité&quot; résume bien les personnes atteintes de ce syndrome.</p>
<p>Beaucoup de questions et remarques en fin de talk, car c'est un sujet universel.</p>
<h3 id="commentaires-1">Commentaires</h3>
<blockquote>
<p>Une partie du sujet avait été abordé à Volcamp, on ne peut pas être insensible à ces propos. Pour être franc, j'avais prévu d'assister à une autre conf plus tech, heureusement, il devrait y avoir le replay ;)</p>
</blockquote>
<h2 id="rendez-lagilité-aux-développeurses-">Rendez l'agilité aux développeur(se)s !</h2>
<p>Une autre conf orienté orga &amp; équipe, avec <a href="https://twitter.com/klf37">Fanny Klauk</a> qui nous donne les &quot;clés&quot; pour qu'une équipe de dev travaille dans de bonnes conditions !</p>
<p>La présentation raconte l'histoire de Lily, jeune développeuse dans une équipe de dev, qui participe à des sprints. On y retrouve pas mal de douleurs comme l'outil de ticketing pas adapté, les KPIs perso versus équipe, les feedbacks clients qui arrivent trop tard, etc.</p>
<p>A chaque étape, une solution est proposée pour que l'équipe, et notamment Lily, se sente bien dans son job et avance sereinement.</p>
<p><img alt="Agilité Devs" class="zoomable" decoding="async" loading="lazy" src="/2022/12/2022-12-01-agilite-dev.webp"></p>
<h3 id="commentaires-2">Commentaires</h3>
<blockquote>
<p>Un autre sujet éloigné de mon quotidien, mais toujours intéressant ! Et ayant croisé Fanny sur d'autres événements &amp; les réseaux sociaux, j'étais curieux de la voir sur scène. Sujet maitrisé devant une salle pleine, avec un ton juste assorti de quelques blagues et légères piques, j'adhère !</p>
</blockquote>
<h2 id="filtrage-des-flux-réseaux-issus-des-clusters-kubernetes--pourquoi-comment-">Filtrage des flux réseaux issus des clusters Kubernetes : pourquoi, comment ?</h2>
<p>J'avais dit que j'essayais d'aller voir des sujets que je ne connaissais pas, là sur le coup, c'est raté ;)</p>
<p>BlueTrusty, partenaire Calico, nous présente les différentes façons de gérer les flux réseaux sortants d'un cluster Kubernetes, avec plusieurs possibilités d'implémentation : Egress Network Policy, firewall classique, etc.</p>
<p>Plusieurs stratégies sont présentées ci-après.</p>
<h3 id="stratégie-1--network-policies">Stratégie 1 : Network Policies</h3>
<p>Utilisation des Network Policies classiques de Kubernetes.</p>
<p>Tant que pas de règles sur un pod, c'est du allow all.</p>
<p>Avec la CNI Calico, il existe des &quot;GlobalNetworkPolicy&quot; pour simplifier la création de règles, car par défaut les règles sont par namespace ce qui peut être vite complexe à gérer. Calico, dans sa version entreprise , permet également de mettre un ordre sur les règles de firewall pour que les administrateurs réseaux s'y retrouvent.</p>
<h3 id="stratégie-2--node-selector">Stratégie 2 : Node Selector</h3>
<p>L'idée est de créer des règles de FW sur les équipements externes au cluster, puis de placer les pods sur un nœud spécifique, qui sera lui filtré.</p>
<p>C'est une solution peu commode, car on perd en fonctionnalité sur le cluster, obligeant à placer les workloads.</p>
<h3 id="stratégie-3--label-selector-et-bridge-mode">Stratégie 3 : Label Selector et Bridge Mode</h3>
<p>Chaque pod a une IP &quot;publique&quot;, et donc on peut filtrer soit par IP de pod, soit par subnet, etc.</p>
<p>Comme la stratégie 2, cela oblige à gérer un pool d'IP &quot;externe&quot;.</p>
<h3 id="stratégie-4--egress-gateway">Stratégie 4 : Egress Gateway</h3>
<p>Le dernier point est d'utiliser un pod dédié, part lequel sort tous les flux réseaux avec du snat.</p>
<h3 id="conclusion">Conclusion</h3>
<p>Au final, il n'y a pas de meilleure stratégie, cela dépend du contexte, comme d'habitude !</p>
<p>Les slides sont disponibles <a href="https://dl.bluetrusty.eu/SREYTAN_DevOpsD-Day_01Dec22.pdf">ici</a> !</p>
<p><img alt="Filtrage Réseau" class="zoomable" decoding="async" loading="lazy" src="/2022/12/2022-12-01-network-k8s.webp"></p>
<h3 id="commentaires-3">Commentaires</h3>
<blockquote>
<p>Le discours est orienté équipes réseaux, qui ont l'habitude de gérer des interfaces firewall, et peu habituées à écrire du yaml pour définir des règles, dommage, car je connais les deux, et ça marche très bien ensemble ;)</p>
</blockquote>
<blockquote>
<p>Vue d'ensemble assez sympa, même si j'aurais aimé aller plus loin dans la technique, le côté Network Policies est &quot;basique&quot;, mais je ne doute pas que <a href="https://twitter.com/bluetrusty_fr">Stephane Reytan</a> saurait aller plus loin dans un deep dive.</p>
</blockquote>
<h2 id="gérer-les-drifts-des-ressources-terraform-grâce-à-la-méthode-gitops">Gérer les drifts des ressources Terraform grâce à la méthode GitOps</h2>
<p>Un autre sujet qui me &quot;passionne&quot; aussi, Terraform &amp; GitOps ensemble ça ne peut être qu'intéressant !</p>
<p><a href="https://twitter.com/katia_tal">Katia Himeur Talhi</a> nous explique ce qu'est un drift, comment le détecter, et le gérer !</p>
<p>Plusieurs solutions : ne rien faire (sic), avoir un workflow dédié, ou bien avoir quelque chose qui permet l'auto-remédiation.</p>
<p>Dans ce contexte, elle nous présente le couple Terraform + Flux2, avec le plugin &quot;Weave GitOps Terraform Controller&quot; en alpha release.</p>
<p>Cet outil permet simplement de gérer son code TF en mode GitOps avec Flux, et donc de bénéficier de flux avec la détection en read only, la validation manuelle, la gestion de dépendances, etc.</p>
<p>Enfin, elle termine par une démo simple qui met bien en avant les avantages de la solution.</p>
<p>Bonus, les slides sont disponibles <a href="https://fr.slideshare.net/KatiaHimeur/gestion-des-drifts-terraform-avec-la-mthode-gitops">ici</a></p>
<p><img alt="Drit GitOps - Crédits @katia_tal" class="zoomable" decoding="async" loading="lazy" src="/2022/12/2022-12-01-drift-gitops.webp"></p>
<h3 id="commentaires-4">Commentaires</h3>
<blockquote>
<p>J'ai découvert Katia lors d'un talk à l'OSXP 2021, sur un sujet Flux2, j'ai été ravi de la revoir sur Marseille avec un sujet équivalent, en allant plus loin avec Terraform. TF Controller est à surveiller, je pense que cela pourra faire un bon sujet de workshop interne.</p>
</blockquote>
<h2 id="kubernetes--la-solution-à-tous-vos-problèmes-it--spoiler-alert--non">Kubernetes : la solution à tous vos problèmes IT ? (Spoiler alert : non)</h2>
<p><a href="https://twitter.com/thomas_rannou">Thomas Rannou</a>, après avoir fait un tour de ce qu'est Kubernetes, nous présente d'autres solutions qui peuvent répondre à des besoins, sans sortir forcément une artillerie lourde.</p>
<p>L'ensemble des solutions présentées sont liées au cloud provider MS Azure.</p>
<h3 id="commentaires-5">Commentaires</h3>
<blockquote>
<p>Désolé, mais je n'ai pas pris de notes, car cela était juste avant ma conf, et je commençais à avoir quelques problèmes oculaires ! Donc niveau concentration, j'étais un peu ailleurs. Promis, je regarderai le replay ;)</p>
</blockquote>
<h2 id="velero--sauvegardez-et-restaurez-proprement-vos-applications-kubernetes">Velero : sauvegardez et restaurez (proprement) vos applications Kubernetes</h2>
<p>A mon tour de présenter Velero ! Rien à dire de particulier, je connais mon sujet (enfin, je crois !) ;)</p>
<p>Les slides sont <a href="https://presentations.verchere.fr/Backup_Velero_DevOps_Dday_2022">ici</a>.</p>
<p><img alt="Velero - Crédits Lucas" class="zoomable" decoding="async" loading="lazy" src="/2022/12/2022-12-01-velero.webp"></p>
<h3 id="commentaires-6">Commentaires</h3>
<blockquote>
<p>J'étais plus stressé par mon état physique que par ma présentation, et du coup, j'étais plutôt détendu, et le public assez intéressé. De bons retours post-conf, ça fait toujours plaisir !</p>
</blockquote>
<h2 id="révolution-ebpf--un-noyau-linux-dynamique">Révolution eBPF : un noyau Linux dynamique</h2>
<p>On finit la journée par un sujet qui m'intéresse particulièrement, eBPF !</p>
<p><a href="https://twitter.com/raphink">Raphaël Pinson</a> de la société Isovalent nous présente eBPF, qui l'utilise, comment, les cas d'usages, sa &quot;limite&quot; avec le kernel Linux.</p>
<p>Au-delà du cas d'usage le plus commun de l'observabilité dans kubernetes, il nous présente d'autres perspectives d'utilisation niveau réseau et sécurité notamment, agrémenté d'exemples et interactions du noyau.</p>
<p><img alt="eBPF" class="zoomable" decoding="async" loading="lazy" src="/2022/12/2022-12-01-ebpf.webp"></p>
<h3 id="commentaires-7">Commentaires</h3>
<blockquote>
<p>Malgré le créneau de fin de journée, qui n'est pas évident car beaucoup de participants sont déjà partis, beaucoup de monde présent pour cette session super intéressante ! Et en plus on a eu droit à des stickers :P</p>
</blockquote>
<blockquote>
<p>Même si je n'ai pas tout suivi (je venais de terminer ma conf), le sujet est passionnant, on sent que c'est un sujet qui va prendre beaucoup de place ces prochaines années. A suivre...</p>
</blockquote>
<h2 id="pour-finir">Pour finir</h2>
<p>Encore une belle édition, même si je n'étais pas à 100% de mes capacités. J'espère y revenir l'an prochain, avec toujours autant d'amis, anciens et nouveaux collègues, partenaires à croiser sur les stands !</p>
<p>Dommage de ne pas avoir pu assister à certains talks, il y en a que j'aurai beaucoup aimé voir, mais je n'ai pas encore le don d'ubiquité...</p>
<p>Rendez-vous en 2023 !</p>]]></content:encoded></item><item><title>Notes Volcamp 2022 - Vendredi</title><link>https://www.vrchr.fr/posts/2022/10/14/volcamp-vendredi/</link><pubDate>Fri, 14 Oct 2022 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2022/10/14/volcamp-vendredi/</guid><description>Retour sur la conférence Volcamp 2022, 1er jour.</description><content:encoded><![CDATA[<h2 id="volcamp-deuxième-journée">Volcamp, deuxième journée</h2>
<p>Après une première journée bien remplie, on rattaque !</p>
<p>Pression supplémentaire, c'est aujourd'hui que je fais une présentation. J'en parlerai dans le paragraphe dédié.
Du coup, les notes sont moins fournies, étant concentré sur ma session.</p>
<p><img alt="Jour 2" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2020-10-14-volcamp-jour-2.webp"></p>
<h2 id="keynote">Keynote</h2>
<p><a href="https://twitter.com/Lcinquin">Ludovic Cinquin</a>, CEO d'Octo, nous prédit l'usage des technos dans les 10 prochaines années, équilibre entre adoption et disruption.</p>
<p>Il avait déroulé cette conf à DevoxxFR quelques mois plus tôt, à laquelle j'avais assisté. Je connaissais donc le sujet, même si mis à jour avec les (nombreux) événéments qui se sont déroulés entre temps.</p>
<p>Pour en savoir plus, il suffit de (re)lire <a href="/posts/2022/04/22/devoxxfr-vendredi/#0900---0920-futurospective-digitale--le-futur-est-il-encore-ce-quil-%c3%a9tait-httpscfpdevoxxfr2022talkbmv-7373futurospective_digitale__le_futur_est-il_encore_ce_que28099il_etait_3f">mes précédentes notes</a> ;)</p>
<p><img alt="Keynote" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-14-keynote.webp">
<em>Crédits Image : <a href="https://www.linkedin.com/posts/regisallegre_coding-sustainability-volcamp2022-activity-6986749787228241920--y9S">Régis Allègre</a></em></p>
<h3 id="commentaires">Commentaires</h3>
<blockquote>
<p>Approche toujours pertinente, et rappeler le concept de right tech est plus que nécessaire !</p>
</blockquote>
<h2 id="comment-casser-sa-prod-avec-des-logs--en-toute-efficacité">Comment casser sa prod avec des logs ? (En toute efficacité)</h2>
<p>Avec plein d'autodérision, le binôme <a href="https://twitter.com/lectem">Clément Grégoire</a>, Cédric Charière--Fiedler nous montrent tous les anti patterns de la gestion des logs.</p>
<p>On y parle de mauvaise rotation, logs trop verbeux, inutiles, et comment l'éviter.</p>
<p>Encore plus loin niveau code, les mauvais usages de code custom qui surcharge les loggers, et qui font au final n'importe quoi.</p>
<p><img alt="Gestion des Logs" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-14-logs.webp"></p>
<h3 id="commentaires-1">Commentaires</h3>
<blockquote>
<p>A voir si vous avez déjà eu un problème de gestion de logs, vous vous y retrouverez à coût sur.</p>
<p>Pour avoir déployé des infrastructures de logs, le sujet me parle beaucoup, et le sujet est traité avec &quot;légèreté&quot;, je recommande !</p>
</blockquote>
<h2 id="comment-nous-avons-géré-le-syndrome-de-fatigue-des-alarmes-en-réduisant-le-bruit-généré-par-nos-alertes">Comment nous avons géré le syndrome de fatigue des alarmes en réduisant le bruit généré par nos alertes.</h2>
<p>Doctolib, représenté par <a href="https://twitter.com/gnublin">Gauthier François</a>, nous fait un retour d'expérience sur le syndrome de l'alarme fatigue, et comment son équipe a réduit drastiquement ces alertes.</p>
<p>Le constat : lors de la migration de leur application vers un environnement conteneurisé, les métriques &amp; alertes n'ont pas été revue. Ainsi, les équipes SRE et astreinte ont vu le volume d'alertes exploser, en lien avec le nombre de composants de leurs clusters.</p>
<p>Il a fallu donc faire du ménage... mais comment ?</p>
<p>Après avoir donné leur définition du bruit (alerte qui se résoud seule au bout de 2min), du budget qu'il souhait mettre (X alertes par trimestre), Gauthier nous fait part de leur méthodologie pour nettoyer tout cela.</p>
<p>De façon simple pragmatique (mot qui définit très bien Gauthier), sont revues la pertinence des alertes, leur duplicité (N alertes pour 1 même problème), les seuils à gérer (les warning ne servent jamais, on repousse toujours jusqu'à une alerte critique où l'on intervient enfin).</p>
<p>On est aussi dans l'amélioration continue, avec des itérations courtes sur les changements, modifications de seuils, etc.</p>
<p>Ainsi le bruit a été réduit drastiquement, jusqu'à avoir un effet pervers : le silence... Inhabituel, jusqu'en oublié l'astreinte !</p>
<p><img alt="Gauthier François" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-14-alerte-fatigue.webp"></p>
<h3 id="commentaires-2">Commentaires</h3>
<blockquote>
<p>Très beau REX simple, sans prétention, l'intervention de Gauthier était claire. Pour une première conf, ce fut une belle surprise !</p>
</blockquote>
<h2 id="velero---sauvegardez-et-restaurez-proprement-vos-applications-kubernetes">Velero - Sauvegardez et Restaurez (proprement) vos applications kubernetes</h2>
<p>A mon tour de monter sur scène !</p>
<p>Première fois que je présente ce sujet, parti d'un constat qu'il manquait de ressources en français sur la configuration de l'outil Velero pour gérer mes backups en environnement conteneurisé. Et le &quot;peu&quot; de ressources type blogpost n'allaient pas suffisamment loin.</p>
<p>J'ai donc essayé de compléter cela avec mon expérience sur le sujet, en y apportant ma compréhension, mes cas d'usages, mes erreurs... Difficile par contre de présenter un produit qu'on connait certes, mais en tant que &quot;simple&quot; utilisateur, n'ayant pas traité tous les uses-cases possibles.</p>
<p>La conf s'est déroulée sans accrocs, dans les temps, avec un public plutôt bienveillant, même si j'ai remarqué beaucoup de baillements dans le public ! Il faut dire que la session était juste après le buffet du midi, avec aligot au menu !!</p>
<p>J'ai déjà remarqué quelques coquilles, améliorations pour la prochaine fois.</p>
<p>Si vous lisez ce blogpost et avait assisté à ma conf, je suis preneur de retour ! Les slides sont également disponibles  <a href="https://presentations.verchere.fr/Backup_Velero/">ici</a>. Si j'ai dit des bétises ou si vous avez des compléments d'information, n'hésitez pas à m'en faire part !</p>
<p><img alt="Rémi Verchère" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-14-velero.webp"></p>
<h2 id="gps-précis-isobus-coupure-de-sections-drones-robots-analyses-la-place-de-la-tech-dans-lagriculture-daujourdhui">GPS précis, ISOBUS, coupure de sections, drones, robots, analyses... la place de la tech dans l'agriculture d'aujourd'hui</h2>
<p>Avec un teasing sur Twitter aussi impressionnant que la longueur du titre de son talk, <a href="https://twitter.com/mathieupassenau">Mathieu Passenaud</a> nous fait un tour des technologies sur les engins agricoles.</p>
<p>Avec des supports videos, exemples, recherches auprès des constructeurs de machines agricoles, on traite des diverses technos disponibles : la gestion de la pression des pneus pour gérer le tassage, la répartition du poids avec les engrais, la détection des produits avec capteurs (tomates, grains, etc.), le déplacement des machines à l'aide du GPS, et GPS RTK.</p>
<p>Même si la techno embarqué n'est pas compliquée, son usage reste impressionnant !</p>
<p>En plus du sujet technique, une vrai sensibilisation autour du travail du monde agricole est faite. Respectez les agriculteurs et ce métier noble de travail de la terre et des animaux !</p>
<p><img alt="Mathieu Passenaud" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-14-agriculture.webp"></p>
<h3 id="commentaire">Commentaire</h3>
<blockquote>
<p>J'attendais ce talk avec impatience, le sujet avait très bien été vendu sur les réseaux sociaux... Et connaissant un tout petit peu le speakers, j'étais sur de ne pas être déçu. Et cela a été confirmé par une salle pleine !</p>
<p>C'était mon dernier talk avant de rentrer, on peut dire que je termine en beauté. Bravo !</p>
</blockquote>
<h2 id="les-à-côté-de-la-conf">Les &quot;à côté&quot; de la conf.</h2>
<p>En plus d'être présent pour suivre les confs, j'ai été sur scène, mais également sur le stand de ma société, qui était partenaire de l'événement.</p>
<p>Fun fact, on m'a proposé à participer à l'orga côté partenaire après avoir été sélectionné, j'ai donc cumulé plusieurs casquettes.</p>
<p>En vrai, il y avait suffisamment de monde pour tenir le stand, ce qui m'a permis de voir suffisament de présentations et faire du &quot;networking&quot;.</p>
<p>Cela m'a permis aussi de rencontrer des collègues, la boite où je travaille actuellement étant assèz grosse.</p>
<p><img alt="Stand Accenture" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-14-stand.webp"></p>
<p>L'avantage des confs &quot;à taille humaine&quot; comme Volcamp, c'est que la proximité speakers/participants est la, et tout le monde reste disponible, sans stress. Je pense que le fait d'avoir participé au camping de speakers l'été dernier a aussi pas mal jouer dans ma relation que je peux avoir avec certains DevRels &amp; speakers plus expérimentés, c'est très sympa.</p>
<p>Aucun couac apercu sur l'orga, franchement bravo. Et on sent les locaux bien dans leurs bottes, c'est appréciable. Avec un point bonus sur le buffet local, on s'est régalé !</p>
<p>Si vous souhaitez assister à une conférence pas trop grosse, sans prise de tête mais avec quand même des sujets techs sérieux, c'est le bon endroit !</p>
<p>Je vous dit très probablement à l'année prochaine !</p>]]></content:encoded></item><item><title>Notes Volcamp 2022 - Jeudi</title><link>https://www.vrchr.fr/posts/2022/10/13/volcamp-jeudi/</link><pubDate>Thu, 13 Oct 2022 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2022/10/13/volcamp-jeudi/</guid><description>Retour sur la conférence Volcamp 2022, 1er jour.</description><content:encoded><![CDATA[<p><img alt="Chaine des Puys" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-13-chainedespuys.webp"></p>
<h2 id="volcamp-première-journée">Volcamp, première journée</h2>
<p>Pendant 2 jours s'est tenue la conférence Volcamp, &quot;Première conférence tech au coeur des volcans d'Auvergne&quot;.</p>
<p>Suite à une première édition en distanciel l'an dernier, c'est une deuxième édition, mais la première en présentiel.</p>
<p>J'ai été agréablement surpris d'être sélectionné pour y donner une présentation sur &quot;Velero : Sauvegardez et Restaurez (proprement) vos applications kubernetes&quot;.</p>
<p>Les organisateurs sont tous des bénévoles du coin, et nous ont concocté un programme au poil !</p>
<p>Ci après un petit récap des sessions du premier jour auxquelles j'ai pu assister.</p>
<h2 id="keynote---et-si-on-apprenait-à-apprendre-et-partager-autrement">Keynote - (et si on apprenait à) Apprendre et partager autrement</h2>
<p><a href="https://twitter.com/aurelievache">Aurélie Vache</a> de la société OVHCloud, un peu stressée pour sa première keynote, nous explique comment, après avoir abandonné ses passions d'ados en début de carrière, a sur les reprendre pendant le COVID, en prenant du temps pour elle pour &quot;gribouiller&quot; pendant cette période un peu spéciale.</p>
<p>Elle se confie sur son parcours pas évident d'ado bègue, et comment elle en est arriver à sortir un bouquin, faire des confs, jusqu'à cette keynote, en remerciant celles &amp; ceux qui l'ont aider jusque là.</p>
<p>En deuxième partie de session elle nous donne quelques pistes pour que nous aussi puissions nous libérer du temps, et reprendre nos passions d'ados, en s'aérant l'esprit.</p>
<p>Tout le monde peut partager, aider, transmettre, sans pression, comme on le sent. Il ne faut pas copier les autres, ni penser qu'on n'a rien à apporter. On dira, montrera les choses toujours sous un angle différent qui pourra toucher certaines personnes.</p>
<p>Enfin, elle insiste sur le fait de rester nous mêmes, ne pas en faire trop, à la course aux likes sur les réseaux sociaux. Le partage doit rester un acte d'altruisme.</p>
<p><img alt="Aurelie Vache" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-13-apprendre-partager-autrement.webp"></p>
<h3 id="commentaires">Commentaires</h3>
<blockquote>
<p>J'ai trouvé son témoignage émouvant, on sentait la sincérité dans son discours. Sur la suite, cela était plein de bon sens et un beau témoignage sur le partage.</p>
<p>En &quot;off&quot;, des collègues ne la connaissant pas ont dit avoir &quot;pris des pieds au cul&quot;, se reconnaissant pas mal dans son parcours, qui ne touche pas que les techs.</p>
<p>Point bonus, j'ai eu la chance d'avoir son bouquin dédicacé 😉</p>
</blockquote>
<h2 id="keynote---soyons-fashion---parlons-de-souveraineté-technologique">Keynote - Soyons Fashion - Parlons de souveraineté technologique</h2>
<p><a href="https://twitter.com/stephmessika">Stephane Messika</a> de la société Cleyrop propose 4 scénarios futuristes sur l'utilisation de la technologie et les problématiques d'accès aux données niveau mondial, sous couvert de souveraineté.</p>
<p>On parle de taxe cloud côté US, chipsest Espion côté Chine, Gestion d'une pandémie, puis survéillance de masse pour atteindre 0 criminalité.</p>
<p>Dans chaque cas se pose la question du calcul bénéfice/risque d'apporter de la techno contre ces menaces, tout en gardant notre indépendance.
Pour éviter ces scénarios, doit on légiférer ? Investir ? Choisr ses combats ? Coopérer entre pays ?
Pleins de questions auxquelles nous n'avons pas de réponses actuellement, mais qui amène à réfléchir...</p>
<p>Un digression est faite sur le HDH, avec les choix fait d'aller sur AWS à un instant T, la problématique de sécurité des données étant resolvable techniquement ainsi, versus des législations complexes inter-état qui dépasse les techs.</p>
<p>On parle enfin French Tech, dont un des rôles justement est d'aider à répondre à ces sujets, en essayant de rester pragmatique.</p>
<h3 id="commentaires-1">Commentaires</h3>
<blockquote>
<p>Le discours était fluide, Stéphane connait son sujet, et j'ai découvert pendant le talk qu'il faisait partie de Gaia-X, qui pour moi est un espace assez nébuleux.</p>
<p>Le talk était un peu trop abstrait pour moi, qui préfère les rex plus techniques.</p>
</blockquote>
<h2 id="comment-jai-développé-le-détecteur-de-deepfakes-le-plus-puissant-du-monde-pour-100">Comment j'ai développé le détecteur de deepfakes le plus puissant du monde pour 100€</h2>
<p><a href="https://twitter.com/MathisHammel">Mathis Hammel</a> nous présente sur super calculateur pour détecter des deepfakes. L'enjeu est de faire tourner le tout sur une carte nvidia jetson nano, en gros un raspberry pi + gpu ;)</p>
<p>L'exercice proposé ici est de vérifier si une photo est réelles, ou bien générée depuis <a href="https://thispersondoesnotexist.com">https://thispersondoesnotexist.com</a></p>
<p>Quelques concepts d'IA sont abordés comme les réseaux de neuronnes, comment entrainer l'IA, les générateurs vs discriminateurs, etc.</p>
<p>Il nous explique ensuite comment détecter qu'un visage est faux : position des yeux, floutage d'arrière plan, hash des images, etc.</p>
<p>On détaille enfin le projet avec l'utilisaton d'un scrapper d'images qui enrichi une base Elastic, l'utilisation de facenet, et comment mettre bout à bout tout cela pour reconnaitre un vrai visage d'un faux.</p>
<p><img alt="Mathis Hammel" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-13-detecteur-deep-fake.webp"></p>
<h3 id="commentaires-2">Commentaires</h3>
<blockquote>
<p>Je voulais au départ aller voir &quot;Service Level &quot;Observability&quot; - in Service Quality we Trust !&quot;, mais la salle étant pleine je me suis rabatu sur cette session.</p>
<p>C'est toujours les sujets que je maitrise le moins qui me surprennent le plus ;)</p>
<p>On peut voir ici que ce n'est pas super complexe pour faire de la détection, avec du bon sens et l'utilisation de bons outils on peut faire des choses sympa sans gros budget... Alors imaginez lorsque vous avez accès à des ressources d'envergure.</p>
<p>J'ai passé un agréable moment à écouté Mathis, il rend les technos qu'il maitrise facilement compréhensibles. Bravo !</p>
</blockquote>
<h2 id="la-sécurité-dès-la-conception-du-projet">La sécurité dès la conception du projet</h2>
<p><a href="https://twitter.com/dadideo">David Aparicio</a> d'OVHCloud nous présente toutes les étapes de la conception de projet où la sécurité doit être implémentée, pour ne pas avoir de surprises après la mise en prod.
Et évidemment, nombreuses sont les étapes à respecter !</p>
<p>Après quelques exemples de non respect de la mise en oeuvre de bonnes pratiques de sécu, David nous dresse une longue liste, mais non exhaustive, des règles à respecter. Plein de cas divers traités comme les configurations par défaut, l'exposition de données personnelles, la gestion du trafic réseau entrant / sortant, etc.</p>
<p>Sont présentés ensuite une autre longue liste d'outil, et à quel moment du process de développement de produits on les met en place.</p>
<p>Avec un tour assez complet des outils &amp; règles, il nous rappelle que la sécurité, comme la documentation, ne doit pas se faire en fin de projet. Et d'ajouter que si la doc n'est pas faite, certes ce n'est pas une bonne chose, mais pourra être plus facilement éditable qu'un logiciel &quot;troué&quot; qui par en prod.</p>
<p><img alt="David Aparicio" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-13-securite-projets.webp"></p>
<h3 id="commentaires-3">Commentaires</h3>
<blockquote>
<p>J'avais loupé cette présentation à plusieurs reprises, je me suis dis que cette fois, c'était la bonne !
David fait un tour très (voir trop) complet des solutions, avec des exemples et rex à l'appui.</p>
<p>Bon tour du propriétaire, mais attention, on sait qu'il y a BEAUCOUP de choses à dire sur le sujet, il faut savoir faire des choix pour ne pas noyer l'auditoire.</p>
</blockquote>
<h2 id="la-grande-démission---est-ce-quon-va-tous-changer-de-job-en-2022-">La grande démission - est-ce qu'on va tous changer de job en 2022 ?</h2>
<p>Session moins tech, on échange avec <a href="https://twitter.com/TheDamfr">Damien Cavailles</a> sur la démission en environnement tech.</p>
<p>Damien nous expose d'abord quelques faits et raisons sur les démissions actuelles, à coup de &quot;cool / pas cool&quot;, comme le contrat psychologique, la mauvaise gestion du travail à distance avec le covid, les risques de burnout, etc.</p>
<p>On a de la &quot;chance&quot; dans nos métiers (du moins, actuellement), de pouvoir facilement démissionner pour aller voir ailleurs.</p>
<p>Mais faut-il encore démissionner pour les bonnes raisons ! A l'inverse des idées préconçues, il considère qu'il vaut mieux démissionner quand tout va bien, plutôt que d'attendre le point de non retour, et partir fâché. Le monde tech étant petit, ce n'est jamais bon signe.</p>
<p>Il propose ensuite 5 critères d'évaluations de nos entreprises, pour vérifier si l'on est bien : salaires, maturité DevOps, culture du blame &amp; espaces safes.</p>
<p><img alt="Damien Cavailles" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-13-grande-demission.webp"></p>
<h3 id="commentaires-4">Commentaires</h3>
<blockquote>
<p>Curieux de connaitre la personne, qui anime We Love Devs, j'ai trouvé la forme &quot;cool&quot;, sur le fond je ne partage pas complètement son avis. Peut être que c'est à nuancer, mais je trouve le coup de démissionner quand tout va bien assez particulier comme concept, même si dans certains cas cela est justifié (proposition qu'on ne peut refuser).</p>
<p>J'ai aussi du mal avec le concept que les démissionnaires sont de bon embassadeurs d'une boite qui va bien. Mais si cela fonctionne pour certains, tant mieux !</p>
</blockquote>
<h2 id="service-mesh-et-kubernetes---la-fausse-bonne-idée-">Service Mesh et Kubernetes - la fausse bonne idée ?</h2>
<p>On va parler d'un sujet qui m'intéresse, les services mesh, présenté par <a href="https://twitter.com/thomas_rannou">Thomas Rannou</a>, <a href="https://twitter.com/kbeaugrand">Kevin Beaugrand</a> !</p>
<p>En prenant comme exemple une application d'e-commerce, avec Istio comme solution mise en oeuvre pour expliquer les concepts du services mesh, on fait le tour du propriétaire.</p>
<p>On utilisera du service mesh pour :</p>
<ul>
<li>De l'observabilité</li>
<li>De la gestion de connexion (re-routage de flux, méchanismes de retry)</li>
<li>De la sécurité (mTLS)</li>
<li>Du contrôle de release (canary / blue-green)</li>
</ul>
<p>Les exemples s'enchainent pour chacun des points, avec l'utilisation de Kiali, la présention de la gestion de retry &amp; canary relaese avec les services spécifiques Istio.</p>
<p>Après le tour du propriétaire, l'équipe explique les limites de l'outil, et la surcharge que cela peut apporter au setup (composants supplémentaires, complexité, maintenace).</p>
<p>Selon le cas d'utilisation, on préconise de ne pas forcément passer directement sur du service mesh, mais parfois utiliser des solutions alternatives plus légères. Si toutefois le service mesh vous intéresse, on aborde l'adoption des features au fur &amp; à mesure.</p>
<p><img alt="Service Mesh" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-13-service-mesh.webp"></p>
<h3 id="commentaires-5">Commentaires</h3>
<blockquote>
<p>Le talk était assez simple de compréhension, très bien pour des gens qui se pose la question s'ils doivent y aller ou pas.
Juste dommage que l'application de démo ait planté ;)</p>
</blockquote>
<h2 id="when-the-little-three-goldies-play-in-the-captain-kubes-zoo">When &quot;The little three goldies&quot; play in the captain kube's zoo</h2>
<p>Un conte revisité pour nous parler de chaos engineering, c'est le pari de <a href="https://twitter.com/sphinxgaiaone">Jérôme Masson</a> !</p>
<p>J'avais eu le privilège de voir la conf de Jérôme au Camping des speakers, sans captation (et sans réseau !!)</p>
<p>Je vous invite alors à lire <a href="/posts/2022/06/19/camping-des-speakers/#quand-les-trois-petits-goldies-jouent-dans-le-zoo-de-capitain-kube-%c3%a9dition-slasher">mes précédentes notes</a>.</p>
<p><img alt="Jérôme Masson" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-13-litmus.webp"></p>
<h3 id="commentaires-6">Commentaires</h3>
<blockquote>
<p>Jérôme, la prochaine fois, arrête de faire des démos qui foirent ;)</p>
</blockquote>
<h2 id="apério-iot">Apério IoT</h2>
<p>Pour le dernier talk de la journée, la fine équipe <a href="https://twitter.com/titimoby">Thierry Chantier</a>, <a href="https://twitter.com/k33g_org">Philippe Charrière</a> &amp; <a href="https://twitter.com/_louidji">Louis Tournayre</a> nous propose une présentation tout en détente sur quelques technos et objets IoT.</p>
<p>Louis étant novice, un échange question / réponse / démo se fait avec Thierry et Philippe, chacun montrant ses &quot;jouets&quot; connectés.
Le tout se fait autour d'un verre et charcuterie, on sent la fin de la journée !</p>
<p><img alt="Apero IoT" class="zoomable" decoding="async" loading="lazy" src="/2022/10/2022-10-13-apero-iot.webp"></p>
<h3 id="commentaires-7">Commentaires</h3>
<blockquote>
<p>J'ai eu quelques retours sur le côté un peu trop dilétente de la présentation, qui dénote par rapport aux précédents talks qui étaient très majoritairement bien préparés et exécutés.</p>
<p>Malgré cette remarque, ce fut un moment de détente agréable en fin de journée, ça ne fait pas de mal ! Surtout qu'étant au 1er rang, nous avons eu droit <strong>aussi</strong> à l'apéro !</p>
</blockquote>
<h2 id="soirée">Soirée</h2>
<p>Le déplacement à Clermont-Ferrand est aussi l'occasion de rencontrer physiquement mes collègues, qui sont venus en force pour l'événement.</p>
<p>C'est donc naturellement que nous avons trinqué ensemble à notre rencontre !</p>
<p>Cela dit, il a fallu être sage, j'avais une conf à réviser pour le lendemain...</p>
<blockquote>
<p><em>Note</em> : comme dans toutes les régions de France, on y boit et mange bien 😅</p>
</blockquote>]]></content:encoded></item><item><title>Kubernetes Nodes Logs avec Promtail</title><link>https://www.vrchr.fr/posts/2022/06/30/promtail-daemonsets-nodes/</link><pubDate>Thu, 30 Jun 2022 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2022/06/30/promtail-daemonsets-nodes/</guid><description>Utilisez les daemonsets Promtail pour récupérer les logs des nodes Kubernetes</description><content:encoded><![CDATA[<p><a href="https://grafana.com/docs/loki/latest/clients/promtail/">Promtail</a> est un agent applicatif permettant de transmettre des logs vers une instance <a href="https://grafana.com/docs/loki/latest/">Loki</a>, outil de gestion de logs de la société Grafana Labs. L'outil fonctionne très bien en environnement Kubernetes, récupérant l'ensemble des logs des conteneurs, avec des notions d'auto-discovery et gestion de labels.</p>
<p>Il est très simple à installer en tant que daemonset dans un cluster, pour y récupérer les logs des conteneurs. Mais parfois on a besoin de récupérer également les logs des nodes du cluster, sur lesquels tournent ces conteneurs.</p>
<p>Ci-après une explication rapide pour la mise en place d'un tel setup.</p>
<p><img alt="Promtail logs with Loki &amp; Grafana" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-30-promtail-logs.webp"></p>
<h2 id="installation-simple">Installation simple</h2>
<p><strong>Note</strong>: Cet article utilise Promtail v2.5.0, la configuration présentée peut différer selon la version que vous utiliserez.</p>
<p>Par défaut, l'installation de Promtail se fait avec un chart Helm, qui fonctionne &quot;out-of-the-box&quot;. On considère que Loki et Grafana sont déjà déployés dans le cluster.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ helm repo add grafana https://grafana.github.io/helm-charts
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ helm repo update
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ helm --install --namespace observability --create-namespace promtail grafana/promtail
</span></span></code></pre></div><p>On peut voir maintenant que les agents Promtail tournent sur les nodes, et que les logs arrivent dans Loki. Grafana permet l'exploration des logs, tout est bon.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl get pods -l app.kubernetes.io/name<span class="o">=</span>promtail
</span></span><span class="line"><span class="ln">2</span><span class="cl">NAME READY STATUS RESTARTS AGE
</span></span><span class="line"><span class="ln">3</span><span class="cl">promtail-47jwn 1/1 Running <span class="m">0</span> 13m
</span></span><span class="line"><span class="ln">4</span><span class="cl">promtail-mwkcb 1/1 Running <span class="m">0</span> 14m
</span></span></code></pre></div><p><img alt="Promtail basic logs" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-30-promtail-basic-logs.webp"></p>
<h2 id="paramétrage">Paramétrage</h2>
<p>Pour l'instant, on a un setup simple, mais fonctionnel.</p>
<p>Cependant, pour les besoins exprimés cela ne suffit pas, car on doit transmettre les logs du répertoire <code>/var/log/</code> des nodes vers Loki, ici plus spécifiquement le fichier <code>/var/log/syslog</code>. Pourquoi ne pas utiliser ces daemonsets pour cela ? On va configurer Promtail en ce sens.</p>
<h3 id="montage-du-dossier-hôte">Montage du dossier hôte</h3>
<p>Tout d'abord, on doit monter le dossier de l'hôte <code>/var/log</code> sur le pod. On montera le dossier dans <code>/var/log/host</code> pour éviter tout problème d'écrasement / nommage.</p>
<p>On spécifie cela dans les paramètres du chart Helm, dans un fichier <code> custom-values.yaml</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># Mount folder /var/log from node</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">extraVolumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">node-logs</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">hostPath</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/var/log</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">extraVolumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">node-logs</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/var/log/host</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">readOnly</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><p>On upgrade le chart helm, et le dossier <code>/var/log/host</code> est bien là, parfait !</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ helm upgrade --install --namespace observability promtail grafana/promtail -f custom-values.yaml
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ kubectl <span class="nb">exec</span> -it promtail-mwkcb --mount <span class="p">|</span> grep /var/log/host
</span></span><span class="line"><span class="ln">3</span><span class="cl">/dev/sda1 on /var/log/host <span class="nb">type</span> ext4 <span class="o">(</span>ro,relatime,data<span class="o">=</span>ordered<span class="o">)</span>
</span></span></code></pre></div><p>Avant d'aller plus loin, voyons si l'on peut lire les fichiers à l'intérieur :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl <span class="nb">exec</span> -it promtail-mwkcb --tail /var/log/host/syslog
</span></span><span class="line"><span class="ln">2</span><span class="cl">tail: cannot open ‘/var/log/host/syslog’ <span class="k">for</span> reading: Permission denied
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">command</span> terminated with <span class="nb">exit</span> code <span class="m">1</span>
</span></span></code></pre></div><p>Mmmm, il y a quelque chose d'incorrect ici, voyons les permissions des fichiers :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl <span class="nb">exec</span> -it promtail-mwkcb — ls -l /var/log/host/syslog
</span></span><span class="line"><span class="ln">2</span><span class="cl">-rw-r---- <span class="m">1</span> <span class="m">102</span> adm <span class="m">72327736</span> Jun <span class="m">29</span> 20:01 /var/log/host/syslog
</span></span></code></pre></div><p>Le fichier appartient à l'utilisateur avec l'ID 102 et groupe &quot;adm&quot;. Avec <code>getent</code> on a un peu plus d'infos sur ce groupe :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl <span class="nb">exec</span> -it promtail-mwkcb -- getent group adm
</span></span><span class="line"><span class="ln">2</span><span class="cl">adm:x:4:
</span></span></code></pre></div><p>Donc, si l'on veut que promtail puisse lire ce fichier, il faut ajouter des contextes de sécurité avec l'option <code>fsGroup</code>, que l'on définit dans les custom values du chart Helm :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># Set fsGroup to allow syslog file reading</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nt">podSecurityContext</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">fsGroup</span><span class="p">:</span><span class="w"> </span><span class="m">4</span><span class="w">
</span></span></span></code></pre></div><p>On upgrade le chart Helm, et on vérifie l'accès au fichier :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ helm upgrade promtail grafana/promtail -f custom-values.yaml
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ kubectl <span class="nb">exec</span> -it promtail-2dbh9 -- tail /var/log/host/syslog
</span></span><span class="line"><span class="ln">3</span><span class="cl">Jun <span class="m">29</span> 20:16:28 worker-pool-node-d5353e kubelet<span class="o">[</span>1596<span class="o">]</span>: I0629 20:16:28.374480 <span class="m">1596</span> clientconn.go:897<span class="o">]</span> blockingPicker: the picked transport is not ready, loop back to repick
</span></span><span class="line"><span class="ln">4</span><span class="cl">Jun <span class="m">29</span> 20:16:28 worker-pool-node-d5353e docker<span class="o">[</span>1888<span class="o">]</span>: I0629 20:16:28.375267 <span class="m">1</span> utils.go:81<span class="o">]</span> GRPC call: /csi.v1.Node/NodeGetVolumeStats
</span></span></code></pre></div><p>Cela semble mieux ! Maintenant on peut lire les fichiers du node depuis notre pod Promtail.</p>
<h2 id="configuration-promtail">Configuration Promtail</h2>
<p>On va ensuit configurer Promtail pour lire (scrape) ce nouveau fichier, avec un simple &quot;File Target Discovery&quot; (voir <a href="https://grafana.com/docs/loki/latest/clients/promtail/scraping/#file-target-discovery">ici</a>). On y rajoute quelques labels pour les retrouver facilement dans Loki plus tard. Toujours dans les custom values :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># Scrape config to read syslog file from node</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">snippets</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">extraScrapeConfigs</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="sd">      # Add an additional scrape config for syslog
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="sd">      - job_name: node-syslog
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="sd">        static_configs:
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="sd">        - targets:
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="sd">          - localhost
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="sd">          labels:
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="sd">            job: node/syslog
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="sd">            __path__: /var/log/host/syslog</span><span class="w">
</span></span></span></code></pre></div><p>On upgrade une fois de plus le chart, et on peut maintenant voir les logs syslog du node, en plus des logs des conteneurs, être transmis dans Loki, nickel !</p>
<p><img alt="Nodes logs with Promtail" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-30-promtail-nodes-logs.webp"></p>
<h2 id="ajout-de-labels-dynamiques">Ajout de labels dynamiques</h2>
<p>Les logs sont là, mais avoir des labels supplémentaires ne serait pas de refus, comme le nom du node.</p>
<p>On va alors utiliser l'option <code>-config.expand-env</code> qui récupère les variables d'environnement, et on va les rajouter dans la configuration de Promtail :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># Allow environment variables usage</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">extraArgs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- -<span class="l">config.expand-env=true</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c"># Scrape config to read syslog file from node</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">snippets</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">extraScrapeConfigs</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="sd">      # Add an additional scrape config for syslog
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="sd">      - job_name: node-syslog
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="sd">        static_configs:
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="sd">        - targets:
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="sd">          - localhost
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="sd">          labels:
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="sd">            job: node/syslog
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="sd">            __path__: /var/log/host/syslog
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="sd">            node_name: &#39;${HOSTNAME}&#39;</span><span class="w">
</span></span></span></code></pre></div><p>On upgrade une dernière fois le chart, on vérifie les logs dans Grafana... Et voilà ! On a bien les logs des nodes, avec les labels qui vont bien !</p>
<p><img alt="Labelled nodes logs with Promtail" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-30-promtail-nodes-logs-labels.webp"></p>
<h2 id="récapitulatif">Récapitulatif</h2>
<p>Pour résumer, afin d'avoir les logs des nodes dans Loki en utilisant Promtail, il &quot;suffit&quot; de :</p>
<ol>
<li>Monter le dossier <code>/var/log</code> du node dans le pod Promtail</li>
<li>Utiliser le bon <code>fsGroup</code> pour pouvoir lire les fichiers</li>
<li>Ajouter une configuration de scraping <code>static_configs</code></li>
<li>Utiliser les variabels d'environnement pour ajouter des labels</li>
</ol>
<p>On retrouvera le fichier <code>custom-values.yaml</code> pour déployer le chart Helm dans mon <a href="https://gitlab.com/rverchere/promtail-host-logs">dépôt GitLab</a>.</p>
<p>On peut maintenant fouiner dans les logs, vérifier ce qu'il ne va pas sur les nodes, et plus encore... Bonne recherche !</p>]]></content:encoded></item><item><title>Camping des Speakers</title><link>https://www.vrchr.fr/posts/2022/06/19/camping-des-speakers/</link><pubDate>Sun, 19 Jun 2022 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2022/06/19/camping-des-speakers/</guid><description>Retour sur les sessions de l&amp;#39;évènement IT qui a chamboulé les normes.</description><content:encoded><![CDATA[<h2 id="une-conf-tech-dans-un-camping-">Une conf tech dans un camping !</h2>
<p><img alt="Logo Camping des Speakers" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-logo_color_text.webp"></p>
<p>Les 9 et 10 Juin 2022 s'est tenu un événement tech pas comme les autres... 2 jours de conférences dans un camping du Morbihan, a.k.a &quot;Le Camping des Speakers&quot; !</p>
<p>Nous étions 4 <del>ex-collègues</del> copains sur place, profitant de cet instant pour partager pleins de belles choses. Voici nos notes (plus ou moins égales) et remarques respectives.</p>
<p>Nous vous souhaitons bonne lecture !</p>
<p><img alt="After" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-after.webp"></p>
<hr>
<h2 id="webassembly-nest-pas-quune-affaire-de-frontend">WebAssembly n'est pas qu'une affaire de frontend</h2>
<p><a href="https://twitter.com/bnj25">Benjamin Coenen</a> de chez Apollo, société qui bosse sur graphql, nous présente WebAssembly et comment on peut l'utiliser hors du navigateur. En effet, quand on parle WASM, on pense surtout aux navigateurs.</p>
<h3 id="historique">Historique</h3>
<p>WebAssembly arrive en 2015, proposé par le w3c. Gros support par Mozilla, puis Fastly qui y voit un intérêt sur la gestion du backend.</p>
<p>WebAssembly est un langage à piles <em>(stack)</em>, on dirait du bon vieil assembleur.</p>
<p>À savoir qu'on ne développe pas en Wasm directement, on utilise d'autres langages qui seront ensuite convertis. Les langages supportés son Rust, Go, C, AssemblyScript, qui permettent de compiler vers Wasm.</p>
<p>Attention pour ceux qui développent en go, wasm ne propose pas de garbage collector.</p>
<p>2 formats de fichiers générés :</p>
<ul>
<li>.wasm : format binaire</li>
<li>.wat : webassembly text</li>
</ul>
<p>Ce que l'on entend dire c'est qu'il est plus performant que JavaScript mais finalement pas tant que ça.</p>
<h3 id="côté-backend">Côté backend</h3>
<p>On peut utiliser Wasm pour divers cas d'usage côté backend : FaaS, smart contract, modules pour d'autres langages, plugins, etc.</p>
<ul>
<li>Acteurs côté FaaS : Suborbital, Wasmcloud</li>
<li>Acteurs côté blockchain : Ethereum</li>
<li>Quelques projets backend : Krustlet, Hippo</li>
</ul>
<h3 id="tools">Tools</h3>
<ul>
<li><strong>wasmer</strong> : pour exécuter du code Wasm, le plus simple à intégrer</li>
<li><strong>wasi</strong> : pour avoir les entrées/sorties. Plus rapidement mis à jour car équipe de Fastly derrière &amp; permet &quot;d'ouvrir les vannes&quot; par rapport au côté sandbox de wasm</li>
</ul>
<p>Binding wasm : utilisation d'une fonction en Rust compilé en wasm puis récupérée en python par exemple <em>(voir wit-bindgen &amp; witgen)</em></p>
<p><img alt="WebAssembly" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-wasm.webp"></p>
<blockquote>
<p>Sujet orienté très dev, même si ce n'est pas mon sujet de prédilection, cela permet au moins d'en savoir un peu plus des possibilités du langage.</p>
<p><a href="https://twitter.com/rverchere">Rémi</a></p>
</blockquote>
<blockquote>
<p>Wasm, Wasi, Wasmer ... Des sujets vus de très (très) loin pour ma part et abordés ici avec beaucoup de détail. Un de ces talk qui donne envie d'en apprendre plus !</p>
<p><a href="https://twitter.com/its_iamcham">Cham</a></p>
</blockquote>
<blockquote>
<p>Sujet côté dev <strong>mais</strong> dont je vois le potentiel pour tout ce qui est FAAS mais pas que! sujet à suivre avec attention.</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<hr>
<h2 id="culture-innovation-et-cartes-ign">Culture, innovation et cartes IGN</h2>
<p>Nous débutons les sessions par un évènement en extérieur animé par <a href="https://twitter.com/bartlettstarman">William BARTLETT</a>. Le groupe est divisé en trois équipes de pondération égale avec chacune un objectif et une méthode pour l'atteindre qui lui est propre.</p>
<h3 id="première-équipe">Première équipe</h3>
<p>La première équipe avait pour objectif de trouver un plot signalétique rouge en ayant comme contrainte de planifier l'ensemble du trajet qui sera réalisé au sein du Camping. Aucun détour ne sera toléré.</p>
<h3 id="deuxième-équipe">Deuxième équipe</h3>
<p>La seconde équipe a pour objectif de trouver les plots signalétiques jaunes en ayant comme contrainte de planifier leur parcours des 100 prochains mètres. Des planifications successives de parcours de 100 mètres pouvaient ainsi être faites jusqu'à l'atteinte de l'objectif.</p>
<h3 id="troisième-équipe">Troisième équipe</h3>
<p>La troisième, dont j'ai fait partie, n'avait <strong>aucun objectif</strong> mais évoluait également avec des contraintes.</p>
<p>Contraintes organisationnelles:</p>
<ul>
<li>Un Chef</li>
<li>Un responsable de la sécurité physique du groupe</li>
<li>Deux sous-groupes d'explorateurs</li>
</ul>
<p>Contraintes dans nos actions:</p>
<ul>
<li>Les explorateurs doivent rester à portée de vue du Chef</li>
<li>Le Chef ne doit pas se déplacer seul</li>
<li>Le Chef peut se déplacer une fois les explorateurs revenus auprès de lui afin de se rendre à un point d'intérêt relevé par un explorateur</li>
</ul>
<h3 id="morale-de-laventure">Morale de l'aventure</h3>
<p>Les trois équipes ont adopté trois méthodes organisationnelles différentes qui pouvaient être associées à:</p>
<ol>
<li>Cycle en &quot;V&quot;</li>
<li>Itérations agiles</li>
<li>Mode anarchique</li>
</ol>
<p>En ayant un objectif précis, nous pouvons passer à côté de belles découvertes imprévues.</p>
<blockquote>
<p>Durant cette activité, William met en valeur l'importance dans le choix organisationnel qui nous permettra de développer notre activité. En ayant un objectif trop spécifique, nous pouvons passer à côté de belles découvertes. Le bon positionnement du curseur entre la recherche de performance et l'ouverture à certaines opportunités est clé dans la réussite. De nombreuses découvertes sont les fruits de projets loupés (ex: <a href="https://fr.wikipedia.org/wiki/Post-it#Une_utilisation_impr%C3%A9vue">Histoire du Post-It</a>).</p>
<p><a href="https://twitter.com/davoult">Sébastien</a></p>
</blockquote>
<hr>
<h2 id="notre-recette-de-léquipe-parfaite-">Notre recette de l’équipe parfaite 🧁</h2>
<p>L'équipe de <a href="https://pix.fr/">PIX</a> avec <a href="https://twitter.com/estelandry">Estelle Landry</a> et <a href="https://twitter.com/YvonnickFrin">Yvonnick Frin</a> au fourneaux, nous font un REX sur leur organisation de dev pour aller de l'idée en production, en mode recette de cuisine (et toque sur la tête ;)).</p>
<p>Quelques points d'idées lors de la conf, le tout dans la bonne humeur :</p>
<ul>
<li>Pas plus de 2 Epic en même temps pour éviter le contexte switching.</li>
<li>Modèle double diamant : phase de découverte/ouverture, puis refermer pour meilleure définition</li>
<li>Grosse phase de discovery, histoire d'être certain que c'est la bonne solution</li>
<li>Une fois que la solution est choisie, création de User Stories pour lancer le dev.</li>
<li>Une bonne user story : atomique et terminée</li>
<li>Découpage le plus petit possible pour du fast delivery (feedback loop and co)</li>
<li>Toilettage et priorisage (en gros tri et priorité)</li>
<li>En daily : on parle des tickets et pas chaque dev</li>
<li>Beaucoup de mob/pair programming</li>
<li>BUS Factor : rendre les personnes dispensables</li>
<li>Ce sont les PO qui gèrent les déploiements. Tout se passe via slack pour le déploiement avec des bots</li>
<li>Paas chez scalingo</li>
</ul>
<p><img alt="PIX" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-equipe_parfaite.webp"></p>
<blockquote>
<p>Beaucoup de sujets / choix d'orga dans lequel je me retrouve, et d'autres que je découvre (j'aime particulièrement le modèle double diamant !). J'ai même une idée pour mon équipe plutôt infra : faire du mob sur de l'IaC, affaire à tester !</p>
<p><a href="https://twitter.com/rverchere">Rémi</a></p>
</blockquote>
<hr>
<h2 id="et-si-on-faisait-le-tour-de-cors--">Et si on faisait le tour de CORS ? ⛵</h2>
<p>Ce <em>talk</em> animé par <a href="https://twitter.com/Cedric_Gatay">Cédric Gatay</a> fait bien sur référence au <strong>mécanisme de restriction d'appel de ressources externes</strong>. Durant 45 minutes, Cédric a pu nous faire une rétrospective sur les raisons de la création de ce mécanisme, puis nous a présenté ses bénéfices, les méthodes pour l'implémenter et son fonctionnement.</p>
<p>Quelques conseils:</p>
<ul>
<li>La <em>Programmation par l'erreur</em> peut sauver, veillez à bien utiliser la console de votre navigateur pour y récupérer l'ensemble des messages.</li>
<li>CORS, son rôle permet juste de dire si cette <em>Origin</em> peut contacter tel <em>Origin</em>.</li>
<li>Les <strong>cors</strong> n'empêchent pas une requête de sortir, mais de lire le résultat côté backend</li>
<li>Il existe 6 entêtes max côté backend</li>
<li><strong>Preflight</strong> (notion importante) et la réponse doivent avoir la même entête</li>
<li>On peut forcer du cross-origin en indiquant l'attribut dans du code html</li>
<li>Content-Security-Policy : au niveau du frontend</li>
</ul>
<p><img alt="Et si on faisait le tour de CORS ?" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-cors.webp" title="Sketchnote"> <a href="https://twitter.com/Marc_DUGUE">@Marc_DUGUE</a></p>
<p><a href="https://docs.google.com/presentation/d/1oW-Fxi7oNPl-GV4g9lymArQLDpqUMIp2BPzeIM6BCsw/edit?usp=drivesdk">Les slides</a></p>
<blockquote>
<p>Sujet à forte technicité pour un profil <em>OPS</em>, mais <em>Cédric</em> à su rappeler le rôle de <strong>CORS</strong> et son champ d'intervention. Bien qu'un sujet initialement adressé aux profils <em>DEV</em> chaque <em>OPS</em> a déjà été confronté à une problématique qu'il n'arrivait pas à interpréter.</p>
<p><a href="https://twitter.com/davoult">Sébastien</a></p>
</blockquote>
<blockquote>
<p>Ayant découvert ce sujet en entreprise sans réellement le comprendre, ce talk m'a permis d'ouvrir les yeux et d'en apprendre davantage sur les CORS. Un beau tour d'horizon très didactique !</p>
<p><a href="https://twitter.com/its_iamcham">Cham</a></p>
</blockquote>
<blockquote>
<p>Sujet également très dev, mais ça permet de mieux comprendre les envois de requête HTTP. Attention les requêtes <strong>CORS</strong> sont liés au navigateur!
Tips: pour faire du <strong>CORS</strong> avec du curl : <code>curl -H 'Origin: xxxx' https://XYZ</code>.</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<hr>
<h2 id="kubernetes--le-coeur-de-la-meule--comme-vous-ne-lavez-jamais-vu">Kubernetes : le coeur de la meule … comme vous ne l’avez jamais vu</h2>
<p>Les DevRels en chef que sont <a href="https://twitter.com/aurelievache">Aurélie Vache</a> et <a href="https://twitter.com/sebi2706">Sébastien Blanc</a> nous font une interprétation à leur sauce de comment fonctionne Kubernetes.</p>
<p>Prenez un chef d'orchestre, des pokeballs, des glaçons, des box Amazon, mélangez le tout et au final vous vous retrouvez avec une explication des Readiness Probes, allez savoir !!</p>
<p>En trois phrases :</p>
<ul>
<li>Epic !</li>
<li>Et si vous mettez des ReadynessProbe/LivenessProbe vous êtes des gens bien</li>
<li>Définissez les Requests et Limits sur vos ressources #Kubernetes , vous serez un bon citoyen du Cloud !</li>
</ul>
<p><img alt="Kubernetes au coeur de la meule" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-kubernetes_meule.webp"></p>
<blockquote>
<p>Façon originale de présenter <strong>Kubernetes</strong> avec comme seuls supports, des boules de jonglage et des boites en carton. Amusant, instructif lorsque l'on ne connait pas le sujet, cette démythification à su ravir les plus joueurs d'entres nous. 😏</p>
<p><a href="https://twitter.com/davoult">Sébastien</a></p>
</blockquote>
<blockquote>
<p>Sujet sur le fond que je connais assez bien, mais beaucoup moins sur la forme ! C'est un peu comme lorsqu'on va voir un film avec nos acteurs préférés : même si le sujet est commun, on sait qu'on passera un bon moment grâce à l'interprétation. Contrat rempli !</p>
<p><a href="https://twitter.com/rverchere">Rémi</a></p>
</blockquote>
<blockquote>
<p>J'ai découvert que l'on pouvait appréhender un sujet complexe d'une manière plus interactive que part des articles ou des vidéos. La scène était parfaite, les acteurs au top et les spectateurs n'en parlons pas! 😛</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<blockquote>
<p>J'ai adoré cramé au soleil en regardant des ballons, poissons et autres jouets voler à côté de la piscine ... tout en parlant de Kubernetes et en déchiffrant des fichiers YAML physique <em>(en papier)</em>. L'esprit du Camping des Speakers résumé en un talk !</p>
<p><a href="https://twitter.com/its_iamcham">Cham</a></p>
</blockquote>
<hr>
<h2 id="comment-jai-monté-le-bureau-ovh-toulouse-après-un-tweet">Comment j'ai monté le bureau OVH Toulouse après un tweet</h2>
<p>Et s'il suffisait d'un Tweet pour changer de vie ? Bon, dis comme ça, cela peut faire penser au synopsis d'un divertissement télévisé.</p>
<p>Mais, sans pour autant changer radicalement de vie, <a href="https://twitter.com/bluxte">Sylvain Wallez</a> nous a conté dans ce talk la naissance du bureau OVH à Toulouse <em>(devinez comment)</em>, les impacts sur sa vie, et pleins d'autres anecdotes croustillantes résumé ici. C'est parti !</p>
<p><img alt="Tweet Octave Klaba" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-klaba.webp"></p>
<p>Donc, le talk, en résumé :</p>
<ul>
<li>Tout est parti <a href="https://twitter.com/bluxte/status/568708583740858368">d'une réponse à un Tweet d'Octave</a> pour déconner</li>
<li>Échanges en MP <em>(là, ça devient sérieux)</em></li>
<li>Constitution d'une équipe de 10 personnes pour monter le PaaS IoT <em>(des copains, contacts avec qui Sylvain avait envie de travailler etc.)</em></li>
<li>Entretien à Roubaix de 9h à 19h <em>(assez intense !)</em></li>
<li>Sylvain est alors pris à l'essai pendant 2 mois à Roubaix <em>(de base, il habite à Toulouse)</em></li>
<li>Il apprend que le bureau va être ouvert à Toulouse <a href="https://twitter.com/olesovhcom/status/613777432068362240">via un Tweet d'Octave</a></li>
<li>Mr. Klaba <em>(le père d'Octave)</em> souhaite faire une visite des locaux le lendemain ! Sylvain a dû tout organiser en quelques heures <em>(chapeau)</em></li>
<li>Visite de 7 locaux et prise de décision en 1 matinée</li>
<li>Petite histoire de la famille Klaba &amp; naissance d'OVH <em>(très instructif !)</em></li>
<li>À la suite d'une visite d'Octave, Sylvain apprend que l'équipe va être doublée via <a href="https://twitter.com/olesovhcom/status/664145976328904705">un nouveau tweet d'Octave</a></li>
<li>2 mois plus tard, le projet PaaS IoT est séparé en 3</li>
<li>Ça n'a pas marché finalement, car le modèle économique s'est révélé incompatible avec la vision d'OVH</li>
<li>Très bonne expérience et très enrichissant cependant</li>
</ul>
<blockquote>
<p>Ce talk ne va pas changer radicalement ma vie, mais au vu du speaker, des anecdotes et des infos hyper intéressantes il va sûrement avoir un impact et y faire écho. Merci pour ce beau retour d'expérience Sylvain !</p>
<p><a href="https://twitter.com/its_iamcham">Cham</a></p>
</blockquote>
<hr>
<h2 id="expérimenter-et-parler-daccessibilité-numérique">Expérimenter et parler d’accessibilité numérique</h2>
<p><a href="https://twitter.com/pansybloom">Virginie Ferey Rochefeuille</a> nous fait prendre conscience de la difficulté d'accès numériques avec quelques jeux de construction lego.</p>
<p>Au programme, 3 groupes avec dans chacun 1 personne en situation de handicap : aveugle, sourd, et sans usage des bras.
Chaque groupe doit alors construire une figurine en lego, avec chacun son handicap.</p>
<p>Par cette expérience, on peut se rendre compte du décalage de la perception que nous avons sur le handicap en tant que personne valide, et cela permet aussi de montrer qu'une personne en situation de handicap physique a toute sa tête, inutile de la considérer différente.
C'est facile à dire, on le sait tous, mais dans la pratique cet atelier nous montre que ce n'est pas tout le temps le cas !</p>
<p><img alt="Atelier Accessibilité Numérique" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-accessibilite.webp"></p>
<blockquote>
<p>J'ai pris le rôle de l'aveugle, et j'ai pu &quot;voir&quot; quelques comportements à mon égard que j'ai trouvé soit inapproprié, soit presque agaçant :
Quand on me dit &quot;prends la pièce à côté&quot;.. je veux bien, mais quel côté ?
Quand on me prend la main pour me guider, sans mon consentement, je me sens considéré autrement...
J'ai alors bien pris conscience du rapport que l'on peut avoir avec l'autre qui est différent, très instructif que de se mettre à la place de l'autre pour mieux le comprendre. JE recommande !</p>
<p><a href="https://twitter.com/rverchere">Rémi</a></p>
</blockquote>
<blockquote>
<p>Pour ma part j'ai dû accompagner une personne ayant les informations, mais n'ayant pas tous ses membres pour réaliser la tâche demandée.
Ma collègue a dû trouver des subterfuges afin de se faire comprendre et adapter son langage pour faire ce qu'elle souhaitait. Inspirant!</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<blockquote>
<p>Quant à moi, je devais accompagner un collaborateur ayant une <strong>perte totale de l'audition</strong> dans la réalisation de la structure. Les barrières étaient nombreuses et nous avons dû adopter un moyen alternatif de communication afin de mener à bien la mission. À travers cette mise en situation, nous prenons conscience qu'il est indispensable d'adapter les postes de travail et de sensibiliser les collaborateurs afin de pouvoir accompagner au mieux les collaborateurs en situation de handicap.</p>
<p><a href="https://twitter.com/davoult">Sébastien</a></p>
</blockquote>
<hr>
<h2 id="indiana-jones-et-les-aventuriers-de-la-conf-perdue">Indiana Jones et les aventuriers de la conf perdue</h2>
<p>Lors de ce <em>Talk</em>, <a href="https://twitter.com/bdauvissat">Benjamin DAUVISSAT</a> nous a présenté l'une des journées les plus longues de sa carrière. À la fois angoissante, perturbante, voir même terrifiante, cela nous rappelle ce qu'il ne faut pas faire en Entreprise.</p>
<ul>
<li>Ne pas perdre le mot de passe <strong>root</strong> du serveur Linux portant toute la <strong>VoIP</strong> de la société</li>
<li>Ne pas rester sur de vieilles versions de <strong>MySQL (5.1)</strong></li>
<li>Ne pas rester dans le déni en sous-estimant le gain de performance apporté par le passage <em>MySQL 5.1 -&gt; MySQL 8</em> (Passage de 10h de syncro à 40 minutes)</li>
<li>Ne pas perdre les <strong>Sources</strong> de ses applications clés</li>
<li><strong>Specs</strong> != <strong>Code Source</strong>. Le Code doit être fait à partir des Specs, pas l'inverse.</li>
</ul>
<p>En cas de doute, connaissez-vous le <a href="https://fr.wikipedia.org/wiki/Test_de_Jo%C3%ABl">Test de Joel</a> ?</p>
<blockquote>
<p>Benjamin nous rappelle qu'une entreprise peut rapidement se laisser déborder si elle ne conserve pas la maitrise de son patrimoine applicatif. L'entreprise se retrouvera tôt ou tard contrainte de corriger la situation qu'elle a laissée s'envenimer, mais cela se fera dans la douleur et généralement au pire moment.</p>
<p><a href="https://twitter.com/davoult">Sébastien</a></p>
</blockquote>
<hr>
<h2 id="comment-jai-aidé-ma-fille-à-lire-avec-le-machine-learning">Comment j'ai aidé ma fille à lire avec le machine learning</h2>
<p>Speaker : <a href="https://twitter.com/vogloblinsky">Vincent Ogloblinsky</a></p>
<blockquote>
<p>Étant papa de deux magnifiques enfants, j'ai à coeur de les accompagner dans leurs apprentissages. Pour moi, une des bases essentielles c'est apprendre à lire.
Alors, trouver un talk mélangeant l'aide à l'apprentissage et technologie, il fallait que j'y aille absolument et j'ai été bluffé!
<a href="https://twitter.com/vogloblinsky">Vincent</a> est parti de ce même constat et a développé une application mélangeant machine learning et application web.</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<p>Résumé :</p>
<ul>
<li>Side project pour sa fille</li>
<li>ML oriented (avec OpenAI)</li>
<li>Difficulté à avoir des voix d'enfant et un dataset convenable
<ul>
<li>Il est allé voir ses contacts du 1er et 2nd cercle pour les faire participer à cette expérience en proposant d'enregistrer certains sons clés. Il en a reçu une 50aine. Après cela il lui a fallu les nettoyer afin que ces sons soient en dessous ou égale à la seconde (1 sec.).</li>
<li>Grâce à cela il a pu implémenté d'autres outils ex: enregistreur pour les sons...</li>
</ul>
</li>
<li>Pour la partie ML (Machine Learning) ce dernier a utilisé un outil codé en Python assez connu dans ce milieu-ci : Tensorflow</li>
<li>Beaucoup de difficulté, mais cela lui a permis de voir énormément de domaines (phonétique, gestion du son, AI/ML etc.)</li>
<li>Le projet est fonctionnel, sur une &quot;petite base&quot;, mais il souhaite pourquoi pas l'étendre et également à d'autres use-cases (personnes étrangères souhaitant apprendre à lire etc.)</li>
</ul>
<p><img alt="Fusion Syllabique" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-machiine_learning.webp"></p>
<blockquote>
<p>Ce projet m'a carrément bluffé!
Tous les points y ont été développés:
&gt; - Comment les sons de la langue française sont composés (alphabet, phonèmes, graphèmes...)
&gt; - Pourquoi il a fait ce projet
&gt; - Comment le ML fonctionne pour les sons
etc.
Un outil bluffant qu'il a fabriqué en partant de zéro.</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<blockquote>
<p>Un side project en 6 mois avec presque &quot;tout à apprendre&quot;, chapeau, vraiment bluffé !</p>
<p><a href="https://twitter.com/its_iamcham">Cham</a></p>
</blockquote>
<hr>
<h2 id="lead-dev-3-ans-dxp-et-alors-">Lead Dev, 3 ans d'xp, et alors ?</h2>
<p>Devenir lead dev avec 3 ans d'expérience, est-ce réellement possible ? Non seulement c'est possible, mais <a href="https://twitter.com/QuesnelLise">Lise Quesnel</a> a donné diverses clés tout au long de son talk sur sa vision du lead dev, son retour d'expérience avec la dimension full-remote imposée par la COVID-19, les difficultés rencontrées ... Tellement pris dedans que je n'ai pu prendre beaucoup de notes !</p>
<p>Notes <em>(clairement non exhaustives)</em> :</p>
<ul>
<li>Techlead <em>(plus axé expertise technique)</em> <strong>≠</strong> lead dev <em>(plus axé accompagnement et soft skills)</em></li>
<li>Difficultés rencontrées :
- Communication externe
- Gestion du temps difficile
- Savoir &quot;trancher&quot; et prendre la &quot;bonne décision&quot;</li>
</ul>
<blockquote>
<p>Sujet et REX intéressants, je suis curieux de revoir les slides !</p>
<p><a href="https://twitter.com/its_iamcham">Cham</a></p>
</blockquote>
<hr>
<h2 id="partage-et-transmission--ratages-et-amélioration">Partage et transmission : ratages et amélioration</h2>
<p>Durant 45 minutes, dans un format de <em>Talk</em> sans <em>Slide</em>, <a href="https://twitter.com/cecilefreydf">Cécile FREYD-FOUCAULT</a> et <a href="https://twitter.com/JoGrenat">Jordane GRENAT</a> ont pu nous transmettre les différentes façons de transmission et de partage nos expertises. Que ce soit au travers de mentoring, partage, formation, les formats sont nombreux, mais les pièges le sont aussi.</p>
<h3 id="le-partage">Le partage</h3>
<p>Le partage consiste à véhiculer le savoir entre ses pairs (personnes de mêmes rangs).</p>
<p>Plusieurs techniques existent, nous avons notamment longuement échangé autour du Pair-Programming:</p>
<ul>
<li>Le Pair Programming permet d' augmenter le savoir (Être et Faire).</li>
<li>Il est nécessaire d'inverser régulièrement les rôles de chacun dans le pair programming afin de casser les routines et éviter les erreurs</li>
<li>Il est important d'être attentif aux autres</li>
<li>Il est intéressant d'avoir une bonne mixité dans les binômes(age, genre, expertise, ...).</li>
<li>Le Pair Programming est <strong>rentable</strong> pour une entreprise</li>
</ul>
<p>Mais cette méthode de <em>Partage</em> peut apporter quelques problèmes:</p>
<ul>
<li>Risque de scission dans les équipes, c'est pourquoi, il est important d'imposer un <strong>roulement</strong> et une <strong>mixité</strong></li>
<li>Vouloir atteindre l' <strong>État parfait</strong></li>
</ul>
<h3 id="la-transmission">La transmission</h3>
<p>La transmission consiste à véhiculer son savoir-être et savoir-faire auprès de personnes inscrites dans un processus d'apprentissage (Étudiants, Apprennant, Mentee ...)</p>
<ul>
<li>Le <strong>Mentor</strong> ne doit pas véhiculer d'idée personnelle ne se reposant sur aucun fondement.</li>
<li>Ne pas faire de <em>Copinage</em> dans un process de <strong>Transmission</strong>, cela posera problème le jour ou un recadrage sera nécessaire.</li>
<li>En cas de création de supports, ce dernier ne <strong>doit pas être autosuffisant</strong>. Il n'est pas nécessaire de reproduire du contenu existant, mais plutôt de capitaliser et d'apporter une <em>valeur</em> complémentaire.</li>
<li>Le <strong>Tuteur</strong> à un engagement de <strong>protection</strong> vis-à-vis de son apprenant par rapport à son environnement , mais également par rapport à l'apprenant lui-même.
<ul>
<li>Horaire de travail</li>
<li>Diversité des taches</li>
<li>Complexité des missions</li>
</ul>
</li>
</ul>
<p><img alt="Camping des Speakers" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-partage_transmission.webp" title="Partage et transmission : ratages et amélioration"></p>
<blockquote>
<p>Cet échange m'a permis de bien différencier la Transmission et le Partage. Les échanges autour des bonnes pratiques permettent d'éviter les pièges dans lesquels nous tombons malheureusement trop souvent...</p>
<p><a href="https://twitter.com/davoult">Sébastien</a></p>
</blockquote>
<hr>
<h2 id="les-5-choses-que-jaurais-aimé-savoir-plus-tôt-dans-ma-carrière">Les 5 choses que j'aurais aimé savoir plus tôt dans ma carrière</h2>
<p>Speaker: <a href="https://twitter.com/sinedied">Yohan Lasorsa</a></p>
<p>Ce talk de 15 min fait partie, pour moi, des talks les plus <strong>inspirants</strong> proposé par le @campingspeakers</p>
<p><strong>Disclaimer</strong>: ici je raconte de ce dont je me suis souvenu, il est possible que tout ne soit pas exactement comme ce qu'il a été raconté.</p>
<ol>
<li>
<p>Se [faire/garder] des contacts</p>
<p>En effet on ne sait pas de quoi la vie sera faite. À travers ce talk <a href="https://twitter.com/sinedied">Yohan</a>
nous signalait, qu'après différentes rencontres, il a pu faire évoluer sa carrière. Il l'a faite évolué en étant recommandé par ses amis d'étude, des gens qu'il a cottoyé à travers différentes conférences, etc.</p>
<p>Ce que je retiens de ce point c'est : soyez bienveillant avec les gens, les personnes inamicaux, ignorer les ! Leur Karma jouera en leur défaveur.</p>
</li>
<li>
<p>Plus facile à dire pardon que demander l autorisation</p>
<p>Ici l'intervenant nous remontait qu'il était plus facile de dire <strong>pardon</strong> pour une action réalisée au lieu de demander l'approbation de la hiérarchie. Souvent la hiérarchie met du temps pour décider. Si la hiérarchie refuse : on dit pardon et on enlève, si elle accepte on a gagné du temps pour réaliser la tache.</p>
<p>Avec ça on &quot;force&quot; la main plus ou moins.</p>
<p>Attention, cela ne s'applique pas à tous les cas de figure 😉</p>
</li>
</ol>
<blockquote>
<p>Tips, à chaque fois que vous souhaitez créer quelque chose en amont : Créer un fichier LICENCE.md dans votre repository.</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<ol start="3">
<li>
<p>Soft skills plus important que la technique</p>
<p>C'est un des points les plus importants de ce talk. Qu'importe que vous mettez du temps à comprendre une techno, une manière de faire, du moment que vous évitez d'être un c*nnard et que vous garder une attitude positive, cela pourra permettre de &quot;sauver&quot; une équipe au bord du précipice (burn out)</p>
<p>Lors d'un projet à grosse pression, un nouvel arrivant avait des difficultés à s'intégrer à l'équipe, mais surtout au projet et être prêt.
Les jours suivants, un red flag a été levé pour signaler le problème et remonter les difficultés que rencontrait l'équipe à lui faire apprendre la techno et l'intégrer au projet. Les responsables de projets n'ont pas répondu à ce red flag, et l'équipe à continuer à le former. Au bout de quelques mois, la personne ayant du mal à l'époque était arrivée au même niveau technique et organisationnel que ceux du projet. Mais durant ces différentes semaines, il a gardé une attitude positive.
Quelques semaines plus tard, la pression devenait plus pesante, mais grâce à l'optimisme du nouvel arrivant, qui ne l'était plus, il a su garder cet état d'esprit (être positif, raconter quelques blagues, relativiser) et cela a apporté à l'équipe. À l'aide de son soft skills, ils ont pu finir le projet dans de meilleures conditions.</p>
<p>En y repensant, la personne ayant remontée le red flag à demandé à ses responsables de ne pas y tenir rigueur, et leur à demandé de l'embaucher pour ses compétences technique mais également ses <strong>soft skills</strong></p>
</li>
<li>
<p>Pour progresser, sortir de sa zone de confort</p>
<p>Je ne vais pas forcément développer ce point. C'est un secret de polichinelle. Pour avancer rapidement dans nos technos/métier il faut se mettre en danger et oser! Et si ça ne fonctionne pas, au moins on aura appris des choses.</p>
</li>
<li>
<p>Pas de certitude dans la tech</p>
<p>Tout le monde souhaite utiliser les dernières technos à la mode. Aujourd'hui, d'après l'intervenant, la librairie JavaScript la plus utilisée sur le web c'est JQuery, le langage coté serveur des sites web : PHP. En termes de techno ce sont des dinosaure, mais celles-ci continuent à évoluer et à répondre au besoin. Alors, pourquoi vouloir changer à part pour la curiosité intellectuelle?</p>
<p>Comme l'a dit <a href="https://twitter.com/ponceto91">Olivier Poncet</a> lors du talk : Si l'application n'est pas cassée ne la répare pas!</p>
</li>
</ol>
<p><img alt="Les 5 choses que j'aurais aimé savoir plus tôt dans ma carrière" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-choses_carriere.webp" title="Yohan Lasorsa en Tong"></p>
<blockquote>
<p>À la fin de ce talk qui devait durer 15 minutes mais qui a durée au final 25 minutes, nous avions pu échanger de manière informelle et en maillot de bain lunette bière. c'était vraiment une bonne expérience.
Ce que j'en retiens de ce talk:
- Oser, quitte à se tromper
- Les erreurs font partie de l'apprentissage
- Être bon dans les technos oui, mais avoir ce petit plus qui peut faire avancer son équipe mieux!
- Être bienveillant et ouvert aux autres et ils vous le rendront</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<hr>
<h2 id="comment-ne-pas-être-un-cnnard-en-5-leçons-">Comment (ne pas) être un c*nnard en 5 leçons 🤗</h2>
<p>La journée a été longue, inspirante, avec des talks techniques, d'autres moins, certains décalés.</p>
<p>L'heure passe, place maintenant à des sujets plus intimes, plus de confidences... Et nous ne sommes pas en reste avec <a href="https://twitter.com/valeriane_IT">Valériane Venance</a> et <a href="https://twitter.com/noel_mace">Noël Macé</a>, qui prennent sur eux pour nous parler de comportements de c*nnard envers les autres.</p>
<p>Au travers d'un dialogue entre 2 individus, ils placent subtilement pas mal de réflexions, clichés, comportements qu'on peut avoir envers les femmes, les minorités diverses et variées, le tout avec un fond de vécu et exemple concrets.</p>
<p>On y parle notamment de &quot;manterrupting&quot;, &quot;manspaining&quot;, &quot;not all X&quot; et l'analogie du sandwich, avec les violences systémiques, et plein d'autres termes pas forcément joyeux à entendre, mais cela fait du bien de remettre en place les choses. On y apprend aussi que tout le monde peut agir contre les c*nnards, éventuellement en être un sans le savoir et donc corriger le tir, et qu'on n'est pas obligé d'être visé par eux pour agir (oui, ça fatigue de toujours répéter les mêmes choses).</p>
<p>Dans un second temps, place à l'échange avec le public, dont beaucoup se retrouvent dans les paroles du duo, et osent parler en public du harcèlement qu'ils peuvent subir au quotidien.</p>
<p><img alt="Comment (ne pas) être un c*nnard en 5 leçons" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-lecons.webp"></p>
<blockquote>
<p>Je n'en ai pas honte, mais ça ma amené à réfléchir, et prendre les actions nécessaire et concrète, pour ne plus être un c*nnard. Le talk sous forme d'un échange au sujet de ce même talk nous amène à une réflexion sur soi et sur ce qui nous entoure.</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<blockquote>
<p>J'ai voulu apprendre à ne plus être un c*nnard. J'ai appris que je pouvais l'être d'une manière bien différente de ce que j'imaginais. Un talk d'exception qui ouvre les yeux sur pas mal de sujets !
<a href="https://twitter.com/its_iamcham">Cham</a></p>
</blockquote>
<blockquote>
<p>Dans ce <em>Track</em> engagé, Noël et Valériane ont su traiter de sujets sensibles avec un esprit critique et constructif. L'objectif était de nous sensibiliser sur des sujets de société dans lesquels nous ne pouvons pas rester spectateurs.</p>
<p><a href="https://twitter.com/davoult">Sébastien</a></p>
</blockquote>
<blockquote>
<p>Sujet très délicat à aborder, mais cela fait du bien (si on peut dire cela) de pouvoir parler librement de ces sujets. En tant qu'homme blanc, hétéro, cadre, je ne suis pas forcément minoritaire, mais j'ai pu voir que cela ne m'empêchait pas d'agir, certain.e.s sont fatigués d'expliquer toujours les mêmes choses, on peut prendre le relais. Bravo en tous cas !</p>
<p><a href="https://twitter.com/rverchere">Rémi</a></p>
</blockquote>
<hr>
<h2 id="améliorer-les-compétences-et-les-infrastructures-avec-les-katas-darchitecture">Améliorer les compétences et les infrastructures avec les katas d'architecture</h2>
<p>Vous connaissez les katas au karaté ? Savez-vous que vous pouvez aussi transposer cette activité pour affiner vos compétences <em>(et vos infrastructures)</em> grâce à l'expérience cumulée de vos collègues et vous-même ?</p>
<p><a href="https://twitter.com/touret_alex">Alexandre Touret</a> dans son talk l'explique très bien et invite tout à chacun à se questionner sur cette discipline intéressante à intégrer <em>(selon lui)</em> dans toute formation interne d'architecture logicielle. Car après tout :</p>
<blockquote>
<p>&quot;Comment sommes-nous censés avoir de grands architectes, s'ils n'ont la chance d'être architectes que moins d'une demi-douzaine de fois au cours de leur carrière ?&quot;</p>
<p><a href="">Ted Neward</a>]</p>
</blockquote>
<p><img alt="Katas" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-kata_architecture.webp"></p>
<p>Par ici les <a href="https://speakerdeck.com/alexandretouret/ameliorer-les-competences-et-les-infrastructures-avec-les-katas-darchitecture-cc629dfb-87f6-4033-aabd-472472ae5655">slides</a> !</p>
<blockquote>
<p>Modèle C4 intéressant et principe de katas tout autant, à creuser, très clairement !</p>
<p><a href="https://twitter.com/its_iamcham">Cham</a></p>
</blockquote>
<hr>
<h2 id="depuis-2-ans-je-suis-la-seule-à-lire-mes-mails--ou-presque">Depuis 2 ans, je suis la seule à lire mes mails ! (ou presque)</h2>
<p>Durant le <em>Talk</em> autour de l'utilisation de solutions mél respectueuses de la vie privée, <em>Morgane Troysi</em> nous présente le fruit de ses recherches d'une solution alternative à <strong>GMail</strong>.
Au travers d'une matrice comparative, Morgane a comparé <em>Proton Mail</em>, <em>Posteo</em>, <em>Mailfence</em> ou encore <em>Tutanota</em> sur des critères comme chiffrement, taille de boite, protocoles, publicité et bien sûr, le prix.</p>
<p><img class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-mails.webp" title="Depuis 2 ans, je suis la seule à lire mes mails ! (ou presque)"> <a href="https://twitter.com/Marc_DUGUE">@Marc_DUGUE</a></p>
<blockquote>
<p>Dans ce Talk traitant du respect de nos libertés individuelles, Morgane a su mettre en lumière que des solutions alternatives au Géant qu' est Google Mail existent. Il aurait été intéressant d'ouvrir plus largement le débat en traitant de notre dépendance aux services des <strong>GAFAM</strong>, peut-être un sujet pour la deuxième édition ? :smirk:</p>
<p><a href="https://twitter.com/davoult">Sébastien</a></p>
</blockquote>
<hr>
<h2 id="développer-un-opérateur-kubernetes-en-java-cest-possible-">Développer un opérateur Kubernetes en Java, c'est possible !</h2>
<p>Sous la forme d'une <a href="https://github.com/philippart-s/camping-java-operator">démo</a> très complète <a href="https://twitter.com/wildagsx">Stéphane Philippart</a> nous a montré comment créer un opérateur Kubernetes en <a href="https://javaoperatorsdk.io/">Java</a> !</p>
<p><img alt="baby DevRel" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-java_operator.webp"></p>
<blockquote>
<p>Très peu de choses à dire pour ce talk vu que la démo était bluffante et le speaker très passionné. J'ai été pris dedans, et ça m'a donné envie de &quot;ressortir mon Quarkus&quot; pour créer un opérateur k8s !</p>
<p><a href="https://twitter.com/its_iamcham">Cham</a></p>
</blockquote>
<blockquote>
<p>Pour ma part, j'ai découvert ce que le SDK Java &amp; Quarkus peuvent réaliser et j'ai vraiment été impressionné en termes de technicité. Le speaker a super bien préparé son talk, et la documentation du projet est tout simplement parfaite !</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<hr>
<h2 id="la-potion-magique-pour-faire-progresser-ta-carrière">La potion magique pour faire progresser ta carrière</h2>
<p><a href="https://twitter.com/dadoonet">David Pilato</a> présente à sa manière tous les bons conseils qu'il a pu avoir pour progresser dans la tech depuis ses débuts.</p>
<p>Vous retrouverez son transcript <a href="https://david.pilato.fr/blog/2022-06-10-la-potion-magique-pour-faire-avancer-ta-carriere/">ici</a>, ce sera plus simple ;)</p>
<p><strong>Spoiler</strong> : l'OpenSource y est pour beaucoup !</p>
<p><img alt="La potion magique pour faire progresser ta carrière" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-carriere.webp"></p>
<blockquote>
<p>100% en phase avec lui, c'est toujours cool de voir qu'on est sur la même longueur d'onde avec des personnes comme David, qui rayonnent sur la tech fr. J'avais fait un talk complémentaire à ce sujet à l'<a href="https://www.youtube.com/watch?v=yVOxMlCXyGs">OpenSource Experience 2021</a>.</p>
<p><a href="https://twitter.com/rverchere">Rémi</a></p>
</blockquote>
<hr>
<h2 id="quand-les-trois-petits-goldies-jouent-dans-le-zoo-de-capitain-kube-édition-slasher">Quand 'Les trois petits goldies' jouent dans le zoo de Capitain kube (édition slasher)</h2>
<p>Un collègue de meetup <a href="https://twitter.com/sphinxgaiaone">Jérôme Masson</a>, nous explique les bonnes pratiques pour avoir une application résiliente dans un contexte Kubernetes.</p>
<p>On parle d'Affinities, de NodeSelector, de Chaos Engineering... mais le tout adapté avec la fameuse histoire des 3 petits cochons !</p>
<p><img alt="Quand 'Les trois petits goldies' jouent dans le zoo de Capitain kube (édition slasher)" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-kubernetes_chaos.webp"></p>
<blockquote>
<p>Belle mise en scène du sujet, avec exemple concret et explications à la clé ! À revoir avec une vraie connexion Internet (et oui, en mode camping on fait avec les moyens du bord !)</p>
<p><a href="https://twitter.com/rverchere">Rémi</a></p>
</blockquote>
<blockquote>
<p>L'histoire des &quot;trois petits cochons&quot; revisitée pour parler Kubernetes et best pactices pour les déploiements d'application : Super bien amené! <strong>Attention</strong> au Slasher!</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<blockquote>
<p>Belle histoire, beau REX suivis d'échanges très sympas. Manquaient plus que le feu et la nuit noire !</p>
<p><a href="https://twitter.com/its_iamcham">Cham</a></p>
</blockquote>
<hr>
<h2 id="mission-impossible--créer-son-pipeline-cicd-en-quelques-minutes">Mission impossible : créer son pipeline CI/CD en quelques minutes</h2>
<p>Speaker : <a href="https://twitter.com/thomas_boni">Thomas Boni</a></p>
<p><a href="https://twitter.com/thomas_boni">Thomas Boni</a> co-créateur de la plateforme r2devops. Plateforme de partage de fichier de Continious Integration et de Best Practices autour de Gitlab &amp; Github.</p>
<p>Durant les 20 min de la conférence <a href="https://twitter.com/thomas_boni">Thomas</a> nous a parlé de la partie pipeline sur Gitlab, non pas en faisant du slideware mais en nous montrant le tout en live!
Une présentation rapide au sujet de <code>.gitlab-ci.yml</code> et de sa documentation (coucou <a href="https://twitter.com/k33g_org">Philippe Charrière</a>).
Après cela une démonstration en live nous a permis de mieux appréhender l'utilité et tous les mécanismes autour de la CI Gitlab.</p>
<p>Cet outil permet, en rentrant dans votre Gitlab <em>(on premise/saas)</em>, de générer des CI à la volée.</p>
<p><strong>Gitflow != Github Flow</strong></p>
<p><strong>Tools</strong> :</p>
<ul>
<li>gitleaks</li>
<li>git flow</li>
<li>github flow</li>
<li>r2devops</li>
<li>mkdoc</li>
</ul>
<p>On retrouve l'ensemble du travail sur GitLab : <a href="https://gitlab.com/r2devops/hub">https://gitlab.com/r2devops/hub</a></p>
<p><img alt="R2devops" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-r2devops.webp"></p>
<blockquote>
<p>Speaker qui maîtrise son sujet, au top!
Un superbe outil à suivre sur.....git....sur Gitlab bien sûr!</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<blockquote>
<p>Je trouve l'idée de la market place de jobs de CI/CD super sympa, et c'est simple mais taguez vos templates c'est toujours utile !</p>
<p><a href="https://twitter.com/rverchere">Rémi</a></p>
</blockquote>
<hr>
<h2 id="comment-bien-foirer-vos-certifications-kubernetes-ckas">Comment bien foirer vos certifications Kubernetes CK{A,S}</h2>
<p>Durant cette session de retour d'expérience <a href="https://twitter.com/rverchere">Rémi VERCHERE</a> nous a présenté en dix points clés <strong>comment bien louper ses certifications</strong> Kubernetes.</p>
<ol>
<li>Trop de confiance</li>
<li>Ne pratiquez pas</li>
<li>N'apprenez ni <code>vim</code>, ni <code>kubectl</code></li>
<li>Ne vous entrainez pas sur <em>killer.sh</em></li>
<li>Ne faites pas de <em>Bookmarks</em> sur <em>kubernetes.io</em></li>
<li>Utilisez votre 1er essai le plus tard possible</li>
<li>Oubliez que vous avez souscrit à l'examen</li>
<li>Ne vous mettez pas dans les conditions de l'examen</li>
<li>Ne lisez pas les questions en entier</li>
<li>N'utilisez pas les contextes K8s</li>
</ol>
<p><img alt="Aahh Remi!" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-kubernetes_certif.webp"></p>
<blockquote>
<p>Ce retour d'expérience par l'échec permet de nous rappeler qu'une excellente connaissance technologique n'est pas toujours suffisante pour décrocher nos certifications. Les préparatifs doivent être sanctuarisés afin d'être le mieux armés pour l'examen, quitte à surperformer le jour J, ce sera l'occasion de vous faire payer une bière par vos amis !</p>
<p><a href="https://twitter.com/davoult">Sébastien</a></p>
</blockquote>
<blockquote>
<p>Il foire ses certifications Kubernetes, mais il n'a pas foiré son talk ! Les 10 commandements cités sont à imprimer et à donner à tout ceux qui souhaitent passer leurs certifications k8s. Et encore une fois, &quot;Go Rémi, Go !&quot; ;)</p>
<p><a href="https://twitter.com/its_iamcham">Cham</a></p>
</blockquote>
<blockquote>
<p>Si vous suivez ce qu'il vous dit c'est sûr que vous allez foirer vos certifications/exams, faites le contraire et vous risquez de les avoir 😉</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<hr>
<h2 id="développer-une-culture-tech-engagée---retour-sur-lbc-première-conférence-tech-par-leboncoin">Développer une culture Tech engagée - retour sur lbc², première conférence tech par leboncoin</h2>
<p>Durant une vingtaine de minutes <a href="https://twitter.com/grillatg">Guillaume GRILLAT</a> nous a présenté comment avec <strong>Le bon coin</strong> ils ont pu libérer la parole sur des sujets de sociétés tels que le réchauffement climatique mais aussi de l'inclusion de la communauté LGBTQ+.</p>
<p>lbc2.fr : La plateforme de conf du bon coin focus sur la tech</p>
<p><img alt="Parler de Tech Engagé" class="zoomable" decoding="async" loading="lazy" src="/2022/06/2022-06-19-camping-lbc2.webp"></p>
<blockquote>
<p><em>Le Bon Coin</em> est un acteur majeur en France dans la fourniture de services aux utilisateurs. C'est près de la moitié des Français qui parcours leur site à la recherche du bon <em>Bien</em> ou <em>Service</em>.
Pour absorber cette charge importante, Guillaume nous a présenté l'organisation qu'ils ont dû adopter et notamment les <strong>70 Features Teams</strong> qui leur permettent de pouvoir être efficients dans leurs <em>MEP</em>.</p>
<p><a href="https://twitter.com/davoult">Sébastien</a></p>
</blockquote>
<blockquote>
<p>L'entreprise d'aujourd'hui n'est plus une entité où l'on fait juste son travail. C'est un lieu où passe la majeure partie de ses journées et où l'on croise des personnes de différentes origines, orientation, etc.
L'entreprise a un rôle sociétal. C'est pourquoi  <a href="https://twitter.com/grillatg">Guillaume GRILLAT</a> est venu nous présenter ce que fait <em>Le Bon Coin</em> en termes d'inclusivité et son rôle au sein de cette société.</p>
<p><a href="https://twitter.com/DavAuff">David</a></p>
</blockquote>
<blockquote>
<p>C'est aussi une des forces du Camping des Speakers, aborder d'autres sujets, tout aussi importants que notre chère tech. J'ai beaucoup aimé les &quot;petites histoires&quot; de Guillaume et les divers tips pour engager des actions concrètes afin que chacun puisse libérer sa parole. Inspirant !</p>
<p><a href="https://twitter.com/its_iamcham">Cham</a></p>
</blockquote>
<hr>
<h2 id="articles-relatifs-à-lévènement">Articles relatifs à l'évènement</h2>
<ul>
<li><a href="https://www.lucienbill.fr/Camping/">Lucien BILL - Le Camping des Speakers - ❤🎉🤯🤿🤩</a></li>
<li><a href="https://alexis-lozano.com/le-camping-des-speakers-2022-1/">Alexis LOZANO - Le Camping des Speakers 2022</a></li>
<li><a href="https://philippart-s.github.io/blog/articles/conf%C3%A9rences/camping-speakers-2022/">Stephane PHILIPPART - Mon Camping des Speaker 2022</a></li>
<li><a href="https://www.grenat.eu/blog/camping-speakers-inoubliable/">Jordane GRENAT - 🇫🇷 Camping des speakers : 1ère édition inoubliable !</a></li>
</ul>
<hr>
<h2 id="les-auteurs">Les Auteurs</h2>
<p>Vous pouvez retrouver les auteurs :</p>
<ul>
<li>Sébastien Davoult : <a href="https://twitter.com/davoult">@davoult</a></li>
<li>Chamseddine Saadoune dit &quot;Cham&quot; : <a href="https://twitter.com/its_iamcham">@its_iamcham</a></li>
<li>David Auffray : <a href="https://twitter.com/DavAuff">@DavAuff</a></li>
<li>Rémi Verchère : <a href="https://twitter.com/rverchere">@rverchere</a></li>
</ul>]]></content:encoded></item><item><title>Requests &amp; Limits avec VPA, Goldilocks et Grafana</title><link>https://www.vrchr.fr/posts/2022/05/16/vpa-goldilocks/</link><pubDate>Mon, 16 May 2022 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2022/05/16/vpa-goldilocks/</guid><description>Utilisez les Vertical Pod Autoscaler, Goldilocks et Grafana pour optimiser les ressources d&amp;#39;un cluster Kubernetes</description><content:encoded><![CDATA[<p>Lorsqu'on développe des applications Cloud Native qui seront hébergées sur Kubernetes, on doit toujours paramétrer et optimiser l'utilisation du CPU et RAM des déploiements.</p>
<p>Si cela n'est pas fait, on peut arriver dans des situations délicates, comme ne pas avoir assez de ressources disponibles si un serveur (<em>node</em>) est indisponible, quelques process <em>OOMKilled</em> à cause de limites de mémoire trop restrictives, des pods non schedulés, etc. De plus, par défaut, certaines valeurs de déploiements ne sont pas toujours alignées avec l'utilisation que l'on en a (comme certains charts Helm publics).</p>
<p>Alors, comment valider nos déploiements, et adapter les Requests &amp; Limits pour coller au mieux à l'utilisation que l'on en a ? Cela peut être fait avec un mix d'outils Open Source, que l'on verra dans cet article.</p>
<h1 id="vertical-pod-autoscaler">Vertical Pod Autoscaler</h1>
<p>Kubernetes Vertical Pod Autoscaler permet de suivre l'utilisation CPU et RAM des pods d'un cluster, et les adapter automatiquement si besoin. On retrouvera plus d'information ici : <a href="https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler">https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler</a></p>
<p>Le VPA a 3 composants: le <strong>Recommender</strong>, l'<strong>Updater</strong> et l'<strong>Admission Plugin</strong>. Dans notre cas, on ne souhaite avoir que des recommendations, alors on n'utilisera que le <strong>Recommender</strong>.</p>
<p>L'installation peut se faire de plusieurs façons, mais on utilisera ici le chart Helm fourni par <a href="https://github.com/FairwindsOps/charts/tree/master/stable/vpa">Fairwinds</a>.</p>
<p>Comme l'on utilise Prometheus comme outil de monitoring, on s'en servira plus tard de source pour récupérer les métriques VPA. On active cela dans les valeurs du chart Helm :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">$ helm repo add fairwinds-stable https://charts.fairwinds.com/stable
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ helm repo update
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ helm install vpa fairwinds-stable/vpa --namespace vpa --create-namespace -f vpa-custom-values.yaml # See below
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">admissionController</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">recommender</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">extraArgs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">prometheus-address</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="sd">      http://prometheus-operated.observability.svc.cluster.local:9090</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">storage</span><span class="p">:</span><span class="w"> </span><span class="l">prometheus</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nt">updater</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span></code></pre></div><p>Comme dit plus haut, Prometheus est utilisé ici comme fournisseur de métriques, pour avoir des informations plus pertinentes, et permettant d'avoir un historique plus grand. On ne couvrira pas l'installation de l'outil dans cet article.</p>
<p>Maintenant qu'on a le VPA fonctionnel (du moins, le recommendeur), on peut créer une configuration VPA. Un exemple est disponible <a href="https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler#example-vpa-configuration">ici</a>.</p>
<p>La définition d'un VPA est assez simple, mais souhaitant plus d'automatisation, on va utiliser <a href="https://www.fairwinds.com/goldilocks">Goldilocks</a> !</p>
<h2 id="goldilocks">Goldilocks</h2>
<p>Goldilocks est un utilitaire de la société <a href="https://www.fairwinds.com/">Fairwinds</a>, qui va permettre de gérer les requêtes et limites simplement, basé sur les VPA, comme précédemment installé.</p>
<p>L'outil va vérifier les déploiements, et automatiquement créer les VPA en fonction de labels. Donc si l'on souhaite activer les VPA sur un déploiement, un namespace ou une autre resource Kubernetes, il suffit d'y ajouter un label, et Goldilocks fera le reste !</p>
<p>L'installation se fait avec un chart Helm :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ <span class="c1"># Goldilocks Helm repository is already there</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ helm install goldilocks --namespace vpa fairwinds-stable/goldilocks
</span></span></code></pre></div><p>Une fois Goldilocks déployé, on retrouve 3 pods :</p>
<ol>
<li>Goldilocks <strong>controller</strong>, qui scrute les labels et créer les VPA en conséquence.</li>
<li>Goldilocks <strong>dashboard</strong>, qui permet d'avoir un dashboard standalone pour voir les reommendations VPA.</li>
<li>VPA <strong>recommender</strong>, qui calcule les recommendations sur les Requests et Limits CPU et RAM.</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl get pods
</span></span><span class="line"><span class="ln">2</span><span class="cl">NAME                          READY   STATUS    RESTARTS   AGE
</span></span><span class="line"><span class="ln">3</span><span class="cl">goldilocks-controller-5bf99   1/1     Running   <span class="m">0</span>          49d
</span></span><span class="line"><span class="ln">4</span><span class="cl">goldilocks-dashboard-699f4    1/1     Running   <span class="m">0</span>          49d
</span></span><span class="line"><span class="ln">5</span><span class="cl">vpa-recommender-74fdc         1/1     Running   <span class="m">0</span>          67d
</span></span></code></pre></div><p>Maintenant, il suffit de rajouter le label qui va bien (<code>goldilocks.fairwinds.com/enabled=true</code>) sur un namespace pour voir la magie opérer : on ajoute ici les labels sur le namespace &quot;velero&quot;, et les VPA seront automatiquement créés :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl label ns velero goldilocks.fairwinds.com/enabled<span class="o">=</span><span class="nb">true</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ kubectl get namespace velero --show-labels                  
</span></span><span class="line"><span class="ln">3</span><span class="cl">NAME     STATUS   AGE    LABELS
</span></span><span class="line"><span class="ln">4</span><span class="cl">velero   Active   112d   goldilocks.fairwinds.com/enabled<span class="o">=</span>true,name<span class="o">=</span>velero
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl">$ kubectl get vpa -n velero                 
</span></span><span class="line"><span class="ln">7</span><span class="cl">NAME                MODE   CPU   MEM         PROVIDED   AGE
</span></span><span class="line"><span class="ln">8</span><span class="cl">goldilocks-restic   Off    15m   <span class="m">183046954</span>   True       67d
</span></span><span class="line"><span class="ln">9</span><span class="cl">goldilocks-velero   Off    11m   <span class="m">183046954</span>   True       67d
</span></span></code></pre></div><p><em><strong>Note:</strong></em> Fairwinds propose aussi un chart Helm qui installe les VPA et Goldilocks en même temps, mais ne le recommendent pas.</p>
<p>Maintenant que l'on a nos VPAs créé automatiquement, on peut voir les recommendations sur le dashboard dédié !</p>
<p><img alt="Goldilocks Dashboard" class="zoomable" decoding="async" loading="lazy" src="/2022/05/2022-05-16-vpa-goldilocks-dashboard.webp"></p>
<p>OK, c'est très bien, mais on a généralement déjà de beaux tableaux de bord avec des outils de monitoring comme le couple Prometheus &amp; Grafana. Pourquoi ne pas les utiliser au lieu du dashboard dédié ? On y va !</p>
<h2 id="prometheus--grafana">Prometheus &amp; Grafana</h2>
<p>En cherchant un peu dans les paramètres des VPA et les métriques disponibles, on peut voir que <code>kube-state-metrics</code> expose les informations suivantes :</p>
<ul>
<li><code>kube_verticalpodautoscaler_status_recommendation_containerrecommendations_lowerbound</code></li>
<li><code>kube_verticalpodautoscaler_status_recommendation_containerrecommendations_upperbound</code></li>
</ul>
<p>Toutes ces métriques sont définies <a href="https://github.com/kubernetes/kube-state-metrics/blob/master/docs/verticalpodautoscaler-metrics.md">ici</a>.</p>
<p>Si on utilise l'opérateur Prometheus, il faudra le mettre à jour pour explicitement activer le collecteur <strong>verticalpodautoscalers</strong>  dans le déploiement <code>kube-state-metrics</code>.</p>
<p><img alt="Prometheus VPA Metrics" class="zoomable" decoding="async" loading="lazy" src="/2022/05/2022-05-16-vpa-goldilocks-prom.webp"></p>
<p>On a maintenant les VPA, les métriques Prometheus, on peut donc arêter le dashboard Goldilocks et créer le notre dans Grafana, et récupérant les bonnes métriques !</p>
<ol>
<li>On va donc mettre à jour le déploiement de Goldilocks pour désactiver le dashboard :</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ helm upgrade goldilocks --namespace vpa fairwinds-stable/goldilocks -f goldilocks-custom-values.yaml
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">dashboard</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span></code></pre></div><ol start="2">
<li>On crée un dashboard Grafana !</li>
</ol>
<p><img alt="Grafana VPA Recommendations" class="zoomable" decoding="async" loading="lazy" src="/2022/05/2022-05-16-vpa-goldilocks.webp"></p>
<p>Cool ! On peut maintenant suivre et adapter les Requests &amp; Limits de nos applications depuis notre outil de dashboard préféré, et adapter les différents seuils pour s'adapter au cluster. On peut aussi étendre l'historique des recommendations, pour mieux adapter les valeurs.</p>
<p>Un exemple de tableau de bord est disponible ici : <a href="https://grafana.com/grafana/dashboards/16294">https://grafana.com/grafana/dashboards/16294</a></p>
<p>Fun fact, j'ai utilisé Goldilocks pour adapter les Requests &amp; Limits de Goldilocks lui-même ;)</p>
<h2 id="ressources">Ressources</h2>
<p>On peut récupérer un autre tableau de bord <a href="https://grafana.com/grafana/dashboards/14588">ici</a>, lequel j'ai utilisé comme source pour crée le mien.</p>]]></content:encoded></item><item><title>Notes Devoxxfr 2022 - Feedback</title><link>https://www.vrchr.fr/posts/2022/04/23/devoxxfr-feedback/</link><pubDate>Sat, 23 Apr 2022 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2022/04/23/devoxxfr-feedback/</guid><description>&lt;p&gt;Après 2 jours intenses de conférence et plus, un feedback sur ma première conférence #DevoxxFR, spéciale 10 ans, et j'espère que ce ne sera pas la dernière !!&lt;/p&gt;
&lt;p&gt;&lt;img class="zoomable" decoding="async" loading="lazy" src="../2022/04/2022-04-22-devoxxfr-vendredi_04.webp"&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<p>Après 2 jours intenses de conférence et plus, un feedback sur ma première conférence #DevoxxFR, spéciale 10 ans, et j'espère que ce ne sera pas la dernière !!</p>
<p><img class="zoomable" decoding="async" loading="lazy" src="/2022/04/2022-04-22-devoxxfr-vendredi_04.webp"></p>
<p>Retrouvez le récap des 2 journées ici :</p>
<ul>
<li><a href="/posts/2022/04/21/devoxxfr-jeudi/">Jeudi 21 avril 2022</a></li>
<li><a href="/posts/2022/04/22/devoxxfr-vendredi/">Vendredi 22 avril 2022</a></li>
</ul>
<h2 id="organisation">Organisation</h2>
<p>Tout d'abord, parlons orga : un grand BRAVO aux gens en rouge, qui ont géré l'organisation, c'était simplement parfait.</p>
<p>On voit qu'il y a 10 ans d'expérience pour gérer une telle machine:</p>
<ul>
<li>Gestion des confs &amp; timekeepers super : j'ai toujours pu assister aux conférences que j'avais sélectionnées (il faut un minimum d'organisation quand même en arrivant à ce genre de conférence)</li>
<li>Stands: il y avait quand même pas mal de monde autour des stands, surtout en milieu de matinée. J'ai bien fait de me lever tôt pour faire le tour avant les confs</li>
<li>Pauses repas sans encombres : même s'il y avait de longues files d'attentes, c'était fluide et je n'ai pas perdu de temps pour assister aux autres confs</li>
</ul>
<p>Aucun soucis à déclarer de ce côté là, je n'ai jamais eu à aller réclamer quelque chose... en général c'est bon signe !</p>
<p>Ma boite organisant les <a href="https://www.devops-dday.com">Devops DDay</a> sur Marseille, je sens qu'on va piquer des idées ;)</p>
<p>Je ne connais pas personnellement les organisateurs, alors je dirai simplement un grand merci à l'ensemble de la team, continuez ainsi !</p>
<h2 id="conférences">Conférences</h2>
<p>Sur un événement de ce style, on a le choix dans les conférences !!</p>
<p>J'avais peur d'avoir des sujets trop orienté dev, finalement la quasi totalité des confs retenues m'ont intéressé, encore un bon point !</p>
<p>Je remarque aussi qu'étrangement, les conférences que j'ai le plus apprécier ne sont pas forcément celles auxquelles je m'attendais.</p>
<p>Belle transition alors sur mon <strong>top 3</strong> des conférences, ci après.</p>
<h3 id="top-3">Top 3</h3>
<ol>
<li>&quot;<strong>Les lois universelles de la performance</strong>&quot; par <a href="https://twitter.com/raphaelluta">Raphaël Luta</a></li>
</ol>
<p>&quot;Bluffé&quot; par l'approche des modèles mathématiques présentés, j'ai été très agréablement surpris. Je ne pensais pas être aussi captivé par le sujet, qui était abordé de manière très pragmatique, encore bravo !</p>
<ol start="2">
<li>&quot;<strong>Qu'avons nous appris après un an passé à développer des opérateurs Kubernetes ?</strong>&quot; par <a href="https://twitter.com/etiennecoutaud">Etienne Coutaud</a></li>
</ol>
<p>Un sujet rondement mené par Etienne, qui a su être didactique sur les composants d'un opérateur. Opération pas évidente.</p>
<ol start="3">
<li>&quot;<strong>Montée de version sans interruption</strong>&quot; par Nelson Dionisi</li>
</ol>
<p>La encore un REX bien expliqué, avec un sujet que je ne pensais pas traité de cet angle (maj BDD)</p>
<p>Etrangement pas de sujets d'observabilité dans mon top 3, mais ces sujets étaient quand même top !</p>
<h3 id="mentions-spéciales">Mentions spéciales</h3>
<ol>
<li>&quot;<strong>Dans les coulisses du 'Cloud'</strong>&quot; par <a href="https://twitter.com/AtaxyaNetwork">Cécile Morange</a></li>
</ol>
<p>Franchement, si tous pouvaient avoir la gniaque et expertise à ton âge, le monde se porterait bien mieux ! Ok il y avait beaucoup de stress senti lors de la présentation, mais OSEF le message est passé !</p>
<ol start="2">
<li>&quot;<strong>Simplifiez vos revues de code avec le rebase interactif</strong>&quot; par <a href="https://twitter.com/sonia_seddiki">Sonia Seddiki</a></li>
</ol>
<p>Un quickie beaucoup trop court, Sonia était très didactique sur un sujet qui ne vend pas forcément du rêve. Pour une première conf c'était plutôt très bon !</p>
<h2 id="rencontres-meet--greet-etc">Rencontres, Meet &amp; Greet, etc.</h2>
<p>Devoxxfr c'est des confs, de la tech, mais aussi des rencontres.</p>
<p>J'ai pu rencontrer des (futurs) collègues d'Accenture, clients, fournisseurs, d'anciens qui - comme moi - ont eu plusieurs vies pros et se retrouvent à Devoxxfr.</p>
<p>J'en ai profité pour rencontrer IRL de nombreuses personnes avec qui j'échange sur twitter, notamment <a href="https://twitter.com/zwindler">@zwindler</a>, <a href="https://twitter.com/damyr_fr">@damyr_fr</a>, <a href="https://twitter.com/louhdetech">@louhdetech</a>, <a href="https://twitter.com/aurelievache">@aurelievache</a>, <a href="https://twitter.com/ponceto91">@ponceto91</a>, <a href="https://twitter.com/k33g_org">@k33g_org</a>, <a href="https://twitter.com/idriss_neumann">@idriss_neumann</a>. C'est un plaisir d'échanger avec vous, même si c'est toujours un peu trop court.</p>
<p>On finit surtout par quelques goodies, des t-shirts, stickers pour les enfants qui sont toujours ravis de me voir revenir pleins de cadeaux inutiles :P</p>
<h2 id="la-suite">La suite</h2>
<p>J'espère bien remettre ça l'an prochain, mais l'année va être chargée, d'autres conférences vont arriver, des CFPs à remplir pour moi aussi partager les quelques trucs que je pense être utiles à diffuser.</p>
<p>Encore un grand bravo à l'équipe, et à l'année prochaine j'espère !</p>]]></content:encoded></item><item><title>Notes Devoxxfr 2022 - Vendredi</title><link>https://www.vrchr.fr/posts/2022/04/22/devoxxfr-vendredi/</link><pubDate>Fri, 22 Apr 2022 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2022/04/22/devoxxfr-vendredi/</guid><description>&lt;p&gt;Après une première journée dense, on continue sur le même rythme, en débutant par 3 keynotes, let's go !&lt;/p&gt;</description><content:encoded><![CDATA[<p>Après une première journée dense, on continue sur le même rythme, en débutant par 3 keynotes, let's go !</p>
<h2 id="0900---0920-futurospective-digitale--le-futur-est-il-encore-ce-quil-était-">09:00 - 09:20 <a href="https://cfp.devoxx.fr/2022/talk/BMV-7373/Futurospective_digitale_:_le_futur_est-il_encore_ce_qu%E2%80%99il_etait_%3F">Futurospective digitale : le futur est-il encore ce qu’il était ?</a></h2>
<p><a href="https://twitter.com/Lcinquin">@Lcinquin</a>, CTO d'Accenture, et ancien PDG d'Octo nous sort sa boule de crystal pour essaye de voir les technos d'avenir, bonne chance !</p>
<p><img class="zoomable" decoding="async" loading="lazy" src="/2022/04/2022-04-22-devoxxfr-vendredi_01.webp"></p>
<h3 id="notes">Notes</h3>
<ul>
<li>Boule de crystal pour essayer de voir les technos de l'avenir</li>
<li>L'IA et le cloud sont des sujets</li>
<li>3 scénarios
<ul>
<li><strong>World tech companies</strong>: Puissance des GAFAM, qui font tout pour qu'on le reste chez eux</li>
<li><strong>Digital Cold War</strong>
<ul>
<li>Chine: population, start up &amp; régime centralisé</li>
<li>UE: marché et régulation</li>
<li>USA: domination culturelle &amp; pure players</li>
<li>Cyber-guerre mondiale ? Avantage à la Chine</li>
</ul>
</li>
<li><strong>Digital Detox</strong>
<ul>
<li>Manque de matière première : de plus en plus compliqué d'avoir du matériel</li>
</ul>
</li>
</ul>
</li>
<li>Vers le right techs -&gt; respet de l'objet, usage, environnement, humain</li>
<li>Le <strong>GROS</strong> sujet : limites de ressources qui vont changer les usages</li>
<li>Le développeur a une responsabilité: le code écrit aujourd'hui sera l'usage de demain</li>
<li>Hacker : réutiliser les ressources dispos</li>
</ul>
<h3 id="conclusion">Conclusion</h3>
<p>Prévisions qui sont certes réalistes, mais pas forcément très rassurantes ! J'aime beaucoup le concept de &quot;right tech&quot;, sujet à creuser.</p>
<h2 id="0925---0945-lesbonsclics-une-plateforme-pédagogique-au-service-du-1er-réseau-européen-daidants-numériques">09:25 - 09:45 <a href="https://cfp.devoxx.fr/2022/talk/SAL-7677/LesBonsclics,_une_plateforme_pedagogique_au_service_du_1er_reseau_europeen_d%27aidants_numeriques.">LesBonsclics, une plateforme pédagogique au service du 1er réseau européen d'aidants numériques.</a></h2>
<p>Thomas VANDRIESSCHE de chez WeTechCare nous présente son travail autour de l'inclusion sociale, via la plateforme &quot;LesBonsClcs&quot;.</p>
<h3 id="notes-1">Notes</h3>
<ul>
<li>WeTechCare : technologie au service de l'inclusion sociale</li>
<li>1/6 personnes n'utilise pas Internet</li>
<li>68% de Français déclarent avoir des pb pour l'usage du numérique</li>
<li>Voir le rapport &quot;Monde social et numérique&quot;</li>
<li>Les bons clics : plateforme pour l'inclusion numérique, avec des howtos, bonne pratiques</li>
<li>de la data dispo pour améliorer les usages et etre acteur dans la société</li>
<li>lien avec les Right Techs</li>
</ul>
<h2 id="0950---1010-la-quête-dune-gouvernance-collaborative-du-web">09:50 - 10:10 <a href="https://cfp.devoxx.fr/2022/talk/FHP-4644/La_quete_d%27une_gouvernance_collaborative_du_web">La quête d'une gouvernance collaborative du web</a></h2>
<p>Le youtubeur <a href="https://www.youtube.com/c/Science4Allfran%C3%A7ais">Lê Nguyên HOANG</a> explique en quelques exemples le poids de l'IA dans la désinformation, et comment y remédier</p>
<h3 id="notes-2">Notes</h3>
<ul>
<li>Sujet sur la désinformation ?</li>
<li>Le problème: on s'informe par ce que les algos leur suggèrent</li>
<li>voir tournesol (plateforme de fakenews)</li>
<li>AI est vulnérable: il ne ressort que ce qu'il a appris, et donc si les &quot;entraineurs&quot; donnent de mauvaises infos, l'AI amplifie ces erreurs.</li>
<li>Plus de faux comptes, c'est plus de fausses voix pour alimenter les IA.</li>
<li>Notion de vallée parabolique pour ne pas trop forcer les votes</li>
<li>Présentation de l'application <a href="https://tournesol.app/">tournesol.app</a></li>
</ul>
<h2 id="1045---1130-comment-opentelemetry-peut-transformer-votre-monitoring-en-unifiant-vos-logsmetricstraces---">10:45 - 11:30 <a href="https://cfp.devoxx.fr/2022/talk/QEB-2923/Comment_OpenTelemetry_peut_transformer_votre_monitoring_en_unifiant_vos_logs%2Fmetrics%2Ftraces">Comment OpenTelemetry peut transformer votre monitoring en unifiant vos logs/metrics/traces</a> - <a href="https://speakerdeck.com/vbehar/traces">💻</a></h2>
<p>Suite au premier volet <em>Opentelemetry</em> de jeudi, on continue ce vendredi avec <a href="https://twitter.com/vbehar">@vbehar</a> qui nous présente la gestion des métriques, logs &amp; traces via le projet OpenTelemetry, comment le mettre en oeuvre, en insistant surtout sur la partie collecteur.</p>
<p><img class="zoomable" decoding="async" loading="lazy" src="/2022/04/2022-04-22-devoxxfr-vendredi_02.webp"></p>
<h3 id="notes-3">Notes</h3>
<ul>
<li>Introduction
<ul>
<li>Bosse chez Ubisoft pour proposer une infra interne de services managés</li>
<li>Implémentation Opentelemetry</li>
<li>Rappel de ce qu'est un log, métriques, traces</li>
<li>Jaeger pour visualiser ces traces</li>
<li>Les 3 piliers de l'observabilité c'est bien, mais ça reste 3 silots car les 3 technos ont leur propres &quot;règles&quot;</li>
<li>Voir cela comme un cube</li>
</ul>
</li>
<li>Opentelemetry
<ul>
<li>Beaucoup de contributeurs</li>
<li>couche commune pour tous les acteurs de solutions d'observabilité</li>
<li>specs, implementations, collecteurs</li>
<li>OpenCensus service, collecteur - killer feature --&gt; à voir</li>
</ul>
</li>
<li>Collecteur: plateforme de &quot;routage&quot;, type logstash
<ul>
<li>permet de récupérer logs, métriques, traces, et les renvoyer vers loki, tempo, prometheus, ...</li>
</ul>
</li>
<li>Grace aux traces, génération de métriques et logs, mais avec de la corrélation</li>
<li>Comment choisir entre les métriques classiques &amp; celles générées par le tracing ? Ca dépend</li>
</ul>
<h3 id="conclusion-1">Conclusion</h3>
<p>Sujet qui complète bien celui de la veille, et montre que les questions d'observabilité et instrumentation dans le codes sont vraiment un point d'attention, continuons ainsi !</p>
<p>J'aime bien le fait d'insister sur le collecteur, car en effet on peut tout à fait combiner traces, métriques, logs. Cependant cela demande un sacré effort d'analyse pour gérer proprement la corrélation.</p>
<h2 id="1145---1230-à-la-découverte-des-docker-dev-environments">11:45 - 12:30 <a href="https://cfp.devoxx.fr/2022/talk/UQC-0994/A_la_decouverte_des_Docker_Dev_Environments">À la découverte des Docker Dev Environments</a></h2>
<p><a href="https://twitter.com/glours">@glours</a> et <a href="https://twitter.com/rumpl">@rumple</a> font le tour de la fonctionnalité &quot;Dev Environment&quot; de Docker Desktop, pour partager un environnement de dev entre développeurs. CQFD.</p>
<h3 id="notes-4">Notes</h3>
<ul>
<li>pb git / dépendance de dev, dur d'avoir un environnnement &quot;propre&quot;</li>
<li>docker est local first, agnostic, le plus simple possible, utilise compose</li>
<li>Setup : clone d'un code source dans un volume docker, détection de language pour une utilisation d'une image prédéfinie, et ça ouvre dans vscode</li>
<li>Possibilité de partager les environnements entre devs</li>
<li>/!\ Attention images arm/i686</li>
<li>Injection du conteneur dans la stack compose</li>
</ul>
<h3 id="conclusion-2">Conclusion</h3>
<p>Fonctionnalité sympa à connaitre, mais pas sur que dans mon cas cela soit utile ;)</p>
<h2 id="1300---1315-profiler-un-pod-dans-kubernetes-avec-kube-flame">13:00 - 13:15 <a href="https://cfp.devoxx.fr/2022/talk/XWG-5167/_Profiler_un_pod_dans_Kubernetes_avec_kube-flame">Profiler un pod dans Kubernetes avec kube-flame</a></h2>
<p>Un quickie sur la génération de flame graphs par <a href="https://twitter.com/loicmathieu">@loicmathieu</a></p>
<h3 id="notes-5">Notes</h3>
<ul>
<li>kubectl flame: profiling de conteneurs en prod, qui génère des flame graphs (projet de yahoo)</li>
<li>s'installe avec krew</li>
<li>de branden gregg</li>
<li>automatise le lancement de profilers, et génère les flames graph au format svg</li>
</ul>
<h3 id="conclusion-3">Conclusion</h3>
<p>Outil très sympa à connaitre, toujours des flame graphs !! Décidément un des sujets à creuser après ce devoxxfr !</p>
<h2 id="1330---1415-les-lois-universelles-de-la-performance---">13:30 - 14:15 <a href="https://cfp.devoxx.fr/2022/talk/SLG-6688/Les_lois_universelles_de_la_performance">Les lois universelles de la performance</a> - <a href="https://speakerdeck.com/rluta/les-lois-universelles-de-la-performance-devoxx-france-2022">💻</a></h2>
<p>Fini les confs autour de l'observabilité, de k8s, je me tente à une conf plus théorique / généraliste, avec celle de <a href="https://twitter.com/raphaelluta">@raphaelluta</a> qui nous présente quelques lois mathématiques applicables à l'informatique.</p>
<p><img class="zoomable" decoding="async" loading="lazy" src="/2022/04/2022-04-22-devoxxfr-vendredi_03.webp"></p>
<h3 id="notes-6">Notes</h3>
<ul>
<li>On parle de système qui est trop lent</li>
<li>bcp de maths</li>
<li>Loi d'amdhal
<ul>
<li>faire un calcul, avoir les résultats avant/après parallélisation, on a alors un modèle pour prédire la charge nécessaire</li>
</ul>
</li>
<li>Anatomie d'une file d'attente (FIFO, etc)
<ul>
<li>Possible aussi de modéliser cela: nb d'élément dans la file, temps d'attente, de traitement, ...</li>
<li>Loi de Little</li>
</ul>
</li>
<li>Loi d'extensibilité universelle, qui prend en compte les perturbations entre les requêtes</li>
<li>mesure de latence avec wrk2 (ou gatling, ou autre)</li>
</ul>
<h3 id="conclusion-4">Conclusion</h3>
<p>Alors pour le coup, j'ai été très agréablement surpris. Pour moi la meilleure conférence que j'ai vu ! Je ne sais pas si c'est parce que je ne connaissais pas le sujet, mais j'ai été captivé tout le long de la présentation.</p>
<p>En gros, ça ne sert à rien d'aligner les CPUs, RAM, Gbps de BP, tout est question de loi mathématiques (ou presque) ;)</p>
<p>Grand bravo à Raphaël !</p>
<h2 id="1430---1515-quavons-nous-appris-après-un-an-passé-à-développer-des-opérateurs-kubernetes-">14:30 - 15:15 <a href="https://cfp.devoxx.fr/2022/talk/VKR-1767/Qu%27avons_nous_appris_apres_un_an_passe_a_developper_des_operateurs_Kubernetes_%3F">Qu'avons nous appris après un an passé à développer des opérateurs Kubernetes ?</a></h2>
<p><a href="https://twitter.com/etiennecoutaud">@etiennecoutaud</a> nous offre un REX sur le développement d'opérateur kubernetes, avec les problèmes qu'on peut avoir lorsqu'on dépasse le &quot;hello world&quot;.</p>
<p>Son expérience est basé sur l'opérateur utilisé par sa société Artifakt, pour déployer des infras cloud.</p>
<h3 id="notes-7">Notes</h3>
<ul>
<li>Artifakt : fait un PaaS multicloud</li>
<li>Sur le PaaS, ajout d'une couche d'asbtraction pour permettre de deployer sur plusieurs types de cloud provider</li>
<li>Utilisation de k8s pour utiliser les CRDs afin de permettre avoir un point d'entrée de pilotage</li>
<li>Plusieurs controleurs dans k8s, sur GCP, qui pilotent les CRDs</li>
<li>En gros, pour déployer des trucs chez Artifakt, on crée un manifest yaml, et basta</li>
<li>Kubernetes, c'est ni plus ni moins que des boucles de réconciliations</li>
<li>status: état observé, spec: état désiré</li>
<li>Ils utilisent Terraform à la fin, piloté par l'opérateur ;)</li>
</ul>
<h4 id="enseignements">Enseignements</h4>
<ol>
<li>bien définir sa CRD</li>
<li>utiliser un framework pour construire le controller</li>
<li>mutating webhook: permet de modifier l'objet avant d'intégrer le cluster (ex istio qui ajoute des sidecar containers)</li>
</ol>
<h3 id="conclusion-5">Conclusion</h3>
<p>Sujet que je voulais absolument voir, et je n'ai pas été déçu. Etienne a été clair dans ses explications, des fois un peu trop denses, mais on sort de la conf en ayant vraiment appris des choses, qui seront utiles pour la suite.</p>
<h2 id="1530---1615-cicd-le-divorce-serait-il-prononcé-">15:30 - 16:15 <a href="https://cfp.devoxx.fr/2022/talk/HBX-3082/CI%2FCD,_le_divorce_serait-il_prononce_%3F">CI/CD, le divorce serait-il prononcé ?</a></h2>
<p>Le couple <a href="https://twitter.com/nicgiro">@nicgiro</a> et <a href="https://twitter.com/YannSchepens">@YannSchepens</a> de chez OnePoint nous racontent l'histoire du couple CI/CD, l'un plutôt Dev, l'autre plutôt Ops.</p>
<p>Ils vont essayer de trouver des solutions pour que les 2 mondes s'entendent bien et puissent avoir des longs jours heureux...</p>
<h3 id="notes-8">Notes</h3>
<ul>
<li>On débute par la CI (TU, contexte développeur), puis la CD
<ul>
<li>D: Delivery, Development, Deployment, pas clair (+ complexe, - automatisé)</li>
</ul>
</li>
<li>CI: plutôt Dev</li>
<li>CD: plutôt Ops</li>
<li>Proposition de pipelines pour que ça marche pour tout le monde
<ol>
<li>Définition des objectifs</li>
<li>Analyse</li>
<li>Cas particuliers</li>
<li>Quels feedbacks veut-on (fastfail) ?</li>
<li>Définir les tâches auto</li>
<li>Choisir l'outillage. Le meilleur n'est pas forcément le plus adapté au contexte</li>
<li>Implémentation</li>
</ol>
</li>
<li>Y aller de manière empirique</li>
<li><strong>Représenter les processus</strong> : Si on ne sait pas représenter, on ne saura pas l'implémenter</li>
<li>Merge des pratiques Dev &amp; Ops</li>
<li>DRY:
<ul>
<li>Factorisation de code, mais pas trop</li>
<li>Librairies de jobs</li>
</ul>
</li>
<li>SOLID</li>
<li>Essayer de faire des pauses intermédiaires dans le cycle CI/CD, pour pouvoir reprendre en cours de route (pas TOUT redérouler sur un fail)</li>
<li>KISS</li>
<li>Ajouter une équipe DevOps : pas forcément le meilleur choix (silo de plus)</li>
</ul>
<h4 id="3-choix-pour-réconcilier">3 Choix pour réconcilier</h4>
<ol>
<li>On sépare Dev &amp; Ops, comme avant, et donc CI &amp; CD séparés, mais au moins on le sait (factuel) et on fait avec</li>
<li>On laisse ainsi, + équipe DevOps qui sera la pour de la médiation technique</li>
<li>Intégration de la culture DevOps au sein des équipes</li>
</ol>
<h3 id="conclusion-6">Conclusion</h3>
<p>Belle histoire, même si on sens les clichés Dev vs Ops, CI vs CD.</p>
<p>Je note surtout le point de la préparation, bien définir ce qu'on souhaite automatiser, avec les bons outils, pas les <em>meilleurs</em></p>
<h2 id="1645---1730-la-fin-des-architectures-en-couches-avec-lapproche-hexagonale">16:45 - 17:30 <a href="https://cfp.devoxx.fr/2022/talk/BTP-8595/La_fin_des_architectures_en_couches_avec_l%E2%80%99approche_hexagonale">La fin des architectures en couches avec l’approche hexagonale</a></h2>
<p>Fini les confs d'infra, pour terminer les sessions un sujet très orienté dev, présenté par Benjamin Legros: l'archi hexagonale !</p>
<h3 id="notes-9">Notes</h3>
<ul>
<li>orienté pour les juniors, pour ne pas se faire avoir avec les concepts appris à l'école, avec l'approche 3-tiers</li>
<li>gestion des jsonViews, jsonIgnore, etc...</li>
<li>contraintes sur la couche de présentation, service &amp; persistences... donc le modèle 3-tiers n'est plus valide</li>
<li>de manière empirique, on arrive à faire du DTO</li>
<li>&quot;Ports &amp; Adapters&quot;, ancêtre de l'archi hexagonale</li>
<li>le coeur de l'archi: le domaine</li>
<li>des ports et des adapters</li>
</ul>
<h3 id="conclusion-7">Conclusion</h3>
<p>Bon, on va pas se cacher, j'ai pas compris grand chose... chacun sa spécialité, mois c'est clairement pas le dev :P</p>
<h2 id="fin-de-la-journée">Fin de la journée</h2>
<p>Après tant de sessions, il est temps pour moi de rentrer, encore une journée riche, avec encore de belles conférences et sujets vu et à creuser !</p>
<p>Un peu plus de détails sur mon feedback général dans le prochain article...</p>
<p><img class="zoomable" decoding="async" loading="lazy" src="/2022/04/2022-04-22-devoxxfr-vendredi_04.webp"></p>]]></content:encoded></item><item><title>Notes Devoxxfr 2022 - Jeudi</title><link>https://www.vrchr.fr/posts/2022/04/21/devoxxfr-jeudi/</link><pubDate>Thu, 21 Apr 2022 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2022/04/21/devoxxfr-jeudi/</guid><description>&lt;p&gt;Pendant trois jours s'est tenue sur Paris la conférence DevoxxFR, qui fétait cette année ses 10 ans.&lt;/p&gt;
&lt;p&gt;10 ans, mais une première pour moi !&lt;/p&gt;
&lt;p&gt;J'ai alors saisi l'occasion pour démarrer ce blog plutôt orienté technique, afin de contribuer à ma manière à la communauté tech française.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Pendant trois jours s'est tenue sur Paris la conférence DevoxxFR, qui fétait cette année ses 10 ans.</p>
<p>10 ans, mais une première pour moi !</p>
<p>J'ai alors saisi l'occasion pour démarrer ce blog plutôt orienté technique, afin de contribuer à ma manière à la communauté tech française.</p>
<p>Vous trouverez ci-après mes notes des 2 jours de conférences (la 1ère journée étant réservée à des sessions plus longs, auxquelles je n'ai pas pu assister).</p>
<p>L'ensemble des notes sont plutôt &quot;brutes&quot;, je laisserai ainsi plutôt que de trop rajouter de bruit au texte.</p>
<p>Vous trouverez également des émoticones suivants pour accéder aux présentations, ressources ou vidéos :</p>
<ul>
<li>💻 : slides</li>
<li>⌨️ : ressources (code, exemples, etc.)</li>
<li>📽️ : vidéos</li>
</ul>
<p><img class="zoomable" decoding="async" loading="lazy" src="/2022/04/2022-04-21-devoxxfr-jeudi_01.webp"></p>
<h2 id="début-de-journée">Début de journée</h2>
<p>La journée démarre dès 7h30, je profite pour faire un peu le tour des stands, c'est le calme avant la tempête. Je ne tarde pas trop car à 8h30 je suis invité au petit-déjeuner GitLab non loin de là.</p>
<p>Premières rencontres avec les copains de twitter, on discute un peu, rencontrons devrels &amp; personnes tenant les stands. Je vais faire un coucou aux futurs copains d'Accenture, et échange avec les lèves-tôt des stands voisins.</p>
<p>Quelques goodies et autres discussions plus tard, il est temps d'aller voir les nombreuses conférences, j'ai déjà loupé les keynotes !</p>
<p><img class="zoomable" decoding="async" loading="lazy" src="/2022/04/2022-04-21-devoxxfr-jeudi_02.webp"></p>
<h2 id="1045---1130-développer-des-applications-observables-pour-la-production">10:45 - 11:30 <a href="https://cfp.devoxx.fr/2022/talk/PFT-6511/Developper_des_applications_observables_pour_la_production">Développer des applications observables pour la production</a></h2>
<p><a href="https://twitter.com/PierreZ">@PierreZ</a> de chez Clever Cloud nous fait un retour d'expérience sur comment il a changé sa manière de développer pour mieux gérer les astreintes, le tout via des applications observables</p>
<p><img class="zoomable" decoding="async" loading="lazy" src="/2022/04/2022-04-21-devoxxfr-jeudi_03.webp"></p>
<h3 id="notes">Notes</h3>
<h4 id="point-sur-lobservabilité">Point sur l'Observabilité</h4>
<ul>
<li>Software is eating the world!</li>
<li>Le logiciel génère de la valeur quand il tourne en prod, pas quand on le dev</li>
<li>Que fait le code quand il tourne ?</li>
<li>Quand on dev, on a une représentation mentale de ce qu'on fait, mais elle peut etre fausse</li>
<li>Tant qu'on ne mesure pas les algos, on ne peut pas savoir ce qui est le mieux</li>
<li>Monitoring : est-ce que ça marche ? Oui/Non</li>
<li>Architecture par service -&gt; distribution des problèmes</li>
<li>Observabilité : à quel point notre système fonctionne bien ?</li>
<li>Papillon -&gt; Ouragan : trouver le papillon = debugging</li>
</ul>
<h4 id="point-sur-le-debugging">Point sur le Debugging</h4>
<ul>
<li>Process qui permet de comprendre le systeme, doit pouvoir se faire en production</li>
<li>Debugger c'est 3 étapes: se poser des questions, faire des observations, revenir au 1)</li>
<li>En tant que dev, écrire du code qui permet de répondre aux questions de debug</li>
<li>code &quot;prod-ready&quot; = code observable</li>
<li>3 méthodes pour debugger : <em>USE</em>, <em>RED</em>, <em>4 Signals</em>
<ul>
<li>Usage, Latency, Saturation, Errors</li>
</ul>
</li>
<li>Comment instrumenter:
<ul>
<li>2 moyens: statique (logs, metrics, traces) &amp; dynamique (ebpf, strace)</li>
</ul>
</li>
<li>Logs: si possible, mettre la mitigation</li>
</ul>
<h4 id="opentelemetry">Opentelemetry</h4>
<ul>
<li>merge entre OpenTracing &amp; OpenCensus</li>
<li>agnostique</li>
<li>librairie qui commence a être vraiment pas mal</li>
<li>plus orienté traces pour le moment</li>
<li>Meilleur outil pour détecter les patterns : notre cerveau</li>
<li>Flame graph: utile pour savoir comment s'enchainent les fonctions, et prennent en %CPU</li>
</ul>
<h3 id="conclusion">Conclusion</h3>
<p>Première conférence très sympa. Même si je connais le sujet, c'est toujours bon de se faire rappeler les bases, et voir qu'on est en phase.</p>
<p>Même si la partie tracing est super intéressante, je pense que les logs et métriques resteront présentes encore un moment, surtout pour les applications qu'on ne maitrisent pas (mode blackbox).</p>
<h4 id="points-à-voir">Points à voir</h4>
<ul>
<li>flamegraph</li>
</ul>
<h2 id="1145---1230-dans-les-coulisses-du-cloud">11:45 - 12:30 <a href="https://cfp.devoxx.fr/2022/talk/RBN-3952/Dans_les_coulisses_du_%22Cloud%22">Dans les coulisses du &quot;Cloud&quot;</a></h2>
<p><a href="https://twitter.com/AtaxyaNetwork">@AtaxyaNetwork</a> nous présente le cloud, de quoi il est fait.</p>
<h3 id="notes-1">Notes</h3>
<ul>
<li>Partage sa passion, n'a pas connu le bug de l'an 2000</li>
<li>Travail chez Aquaray (18 pers), infogérance LAMP. Propre DC, fibre, etc.</li>
<li>Raconte son quotidien
<ul>
<li>L1 au L3, cablage, etc...</li>
</ul>
</li>
<li>Explication de ce qu'est un DC : tout et n'imp', tant que ça héberge de la data</li>
<li>Comment on juge la qualité ? cf TIER
<ul>
<li>tolérance aux pannes</li>
<li>redondance</li>
<li>clim</li>
</ul>
</li>
<li>Comment on est certifié TIER4?
<ul>
<li>Systèmes de refroidissement redondants, topologie cold corridor</li>
<li>2 voies électriques</li>
<li>Redondance réseau (DC propre + redondance sur 2 autres DC - TH2 et Interxion)</li>
</ul>
</li>
<li>Redondance réseau ? On enchaine sur Internet!
<ul>
<li>Meme si réseau maillé, y a qd meme une hiérarchie (tier1)</li>
<li>Qui gère les IP ? RIPE, en EU RIPE NCC</li>
</ul>
</li>
<li>BGP
<ul>
<li>2x plus vieux que la speakeuse ;)</li>
<li>Pas de sécurité par défaut (historique, seulement géré par la confiance)</li>
<li>RFC 7454 donne les bonnes pratiques</li>
<li>Pour sécuriser BGP : RPKI</li>
<li>Ajout de clé privé pour prouver l'identité de l'AS</li>
<li>Le cas cloudfare: mauvaise annonce BGP</li>
</ul>
</li>
</ul>
<h3 id="conclusion-1">Conclusion</h3>
<p>Une jeune de 21 ans qui partage sa passion c'est toujours très agréable à voir ! Malgré un <em>gros</em> stress, on voit que Cécile maitrise son sujet et aime transmettre. Le sujet en soi était assez sympa, venant du réseau &amp; système, j'ai quand même appris des trucs sur BGP !</p>
<h2 id="1300---1315-simplifiez-vos-revues-de-code-avec-le-rebase-interactif">13:00 - 13:15 <a href="https://cfp.devoxx.fr/2022/talk/YOC-8839/Simplifiez_vos_revues_de_code_avec_le_rebase_interactif">Simplifiez vos revues de code avec le rebase interactif</a></h2>
<p>15 minutes qui sont passées trop vite, où <a href="https://twitter.com/sonia_seddiki">@sonia_seddiki</a> nous explique les cas d'usages d'un <code>git rebase -i</code></p>
<p><img class="zoomable" decoding="async" loading="lazy" src="/2022/04/2022-04-21-devoxxfr-jeudi_04.webp"></p>
<h3 id="notes-2">Notes</h3>
<ul>
<li>Merge Request: toujours laborieux car on n'a pas le temps de vérifier que tout est bon
<ul>
<li>Niveau 0 &quot;strict minimum&quot;: strict minimum (convention, propreté)</li>
<li>Niveau 1 &quot;historique&quot;: trace des développements, contexte</li>
</ul>
</li>
<li>Le rebase interactif : rendre en 1 commit plusieurs commits</li>
<li>plusieurs mots clés
<ul>
<li>pick (par défaut)</li>
<li>reword: renommer le message de commit</li>
<li>edit</li>
<li>fixup: reorganiser l'historique</li>
</ul>
</li>
</ul>
<h3 id="conclusion-2">Conclusion</h3>
<p>Je reste sur ma faim, très bonne session, c'est dommage ! Bon en même temps ça tombe bien c'est l'heure de la pause repas ;)</p>
<h2 id="1330---1415-protéger-son-organisation-des-attaques-par-le-système-de-build">13:30 - 14:15 <a href="https://cfp.devoxx.fr/2022/talk/ZWH-7741/Proteger_son_organisation_des_attaques_par_le_systeme_de_build">Protéger son organisation des attaques par le système de build</a></h2>
<p>Dans un contexte plus dev/java @ljacomet nous montre comment bien protéger son système de build.</p>
<h3 id="notes-3">Notes</h3>
<ul>
<li>supply chain n'est plus une hypothèse</li>
<li>exemples de corruption via supply chain : ccleaner, homebrew, solarwinds</li>
<li>le hack se fait maintenant toujours par manière détournée, via les fournisseurs de soft</li>
<li>voir &quot;confusing dependencies at scale in 2020 (alex.birsan sur medium)</li>
<li>Contexte sur l'ensemble des attaques possibles</li>
<li>Gestion des PR: Attention, il peut meme y avoir des pb lors des jobs de CI sur l'infra lors des tests de PR</li>
<li>Seulement build sur l'infra interne après review humaine</li>
<li>Pull Request Acceptance: CLAs, vérification de l'auteur de la PR, vérif &quot;à la main&quot;, ou bien run sur environnement isolé</li>
<li>Signer les commits!</li>
<li>Améliorer la CI:
<ul>
<li>disposable containers, voir build cache</li>
</ul>
</li>
</ul>
<p>Sur les artfacts: voir l'intégrité  + checksum -&gt; récupérer le binaire d'un côté, le checksum d'un autre !</p>
<h3 id="conclusion-3">Conclusion</h3>
<p>Sujet pas super rassurant, on voit qu'on peut être attaqué par tous les côtés !! Cela dit quelques bonnes pratiques à prendre que j'ai notée, à voir comment appliquer cela à nos infras.</p>
<h3 id="points-à-voir-1">Points à voir</h3>
<ul>
<li><em>sigstore</em> pour la signature des binaires</li>
</ul>
<h2 id="1430---1515-cloud-public-mais-données-privées-">14:30 - 15:15 <a href="https://cfp.devoxx.fr/2022/talk/YKJ-2069/Cloud_public,_mais_donnees_privees">Cloud public, mais données privées !</a></h2>
<p><a href="https://twitter.com/GillesSeghaier">@GillesSeghaier</a>, accompagné par le youtuber <a href="https://twitter.com/Micode">@Micode</a> nous font un topo sur les aspects sécurité/confidentialité/disponibilité des données sur le cloud.</p>
<h3 id="notes-4">Notes</h3>
<ul>
<li>Accélération des &quot;cloud outage&quot;</li>
<li>Quand 1 cloud provider tombe, si tout les outils sont liés, grosse dépendance, à faire attention</li>
<li>outage: quand c'est &quot;down&quot; c'est la partie émergé de l'iceberg (gestion des données non dispo)</li>
<li>leak des données: important aussi</li>
<li>demo astrachain</li>
</ul>
<h3 id="conclusion-4">Conclusion</h3>
<p>Désolé mais je n'ai pas super accroché à la conférence. D'une part parce que j'ai trouvé la 1ère moitié de la présentation trop longue, et d'autre part parce que j'avais déjà assisté à une session plus technique d'Astrachain au Devops DDay 2021, dont la vidéo est ici: <a href="https://www.youtube.com/watch?v=afp06HozxA4">https://www.youtube.com/watch?v=afp06HozxA4</a></p>
<h2 id="1530---1615-eliminez-la-complexité-de-kubernetes-avec-lens-">15:30 - 16:15 <a href="https://cfp.devoxx.fr/2022/talk/RAX-2612/Eliminez_la_complexite_de_Kubernetes_avec_LENS">Eliminez la complexité de Kubernetes avec LENS !</a></h2>
<p>L'équipe de @mirantis nous présente l'outil Lens, que je connais bien et utilise au quotidien.</p>
<h3 id="notes-5">Notes</h3>
<ul>
<li>IDE Kubernetes numero 1</li>
<li>plus de 100K utilisateurs</li>
<li>Utiliser k8s pour un dev c'est difficile -&gt; Lens facilite tout cela</li>
<li>95% des utilisateurs trouvent que Lens facilite l'adoption et l'apprentissage</li>
<li>Développé en Electron, facilite les plugins</li>
<li>Tour du propriétaire de l'outil</li>
</ul>
<h3 id="conclusion-5">Conclusion</h3>
<p>Mon ancien collègue Daniel Virassamy est sur scène, j'y suis un peu pour l'encourager, même si la salle est comble !</p>
<h3 id="points-à-voir-2">Points à voir</h3>
<ul>
<li>k0s pour cluster local</li>
<li>extension debug pod!</li>
</ul>
<h2 id="1645---1730-montée-de-version-sans-interruption">16:45 - 17:30 <a href="https://cfp.devoxx.fr/2022/talk/DKU-3342/Montee_de_version_sans_interruption">Montée de version sans interruption</a></h2>
<p>Nelson DIONISI de chez @mirakl nous fait un retour d'xp sur la gestion des mise à jour de leurs applications, avec un focus sur les upgrades de BDD.</p>
<p><img class="zoomable" decoding="async" loading="lazy" src="/2022/04/2022-04-21-devoxxfr-jeudi_05.webp"></p>
<h3 id="notes-6">Notes</h3>
<ul>
<li>Avant 2017, interruption de service lors d'upgrade : 1h = 125K€ de perte
<ul>
<li>1 release tous les 3 mois, grosses montées de version</li>
<li>Trop de pbs -&gt; migration sans interruption !! Use case de la BDD</li>
</ul>
</li>
<li>Règle primordiale: version N+1 de la BDD doit etre <strong>retro-compatible</strong> avec la version N de l'application</li>
<li>En fait, un upgrade se fait en plusieurs étapes, pas d'un seul coup</li>
<li>Exemples dans les divers frameworks</li>
<li>jamais de &quot;select *&quot;</li>
<li>Sur les applis en fort trafic -&gt; attention aux locks de la BDD</li>
<li>Identifier les opérations SQL lentes vs rapides pour optimiser les upgrades</li>
<li>! Foreign key</li>
<li><strong>mieux vaut casser la migration que l'application</strong></li>
</ul>
<h3 id="conclusion-6">Conclusion</h3>
<p>Je pensais qu'on allait parler canary release, blue-green deployment, pas du tout ! Ce fut très enrichissant de voir comment est géré un upgrade côté soft, et pas <em>que</em> deploiement.</p>
<h2 id="1745---1815-git-back-to-the-future---">17:45 - 18:15 <a href="https://cfp.devoxx.fr/2022/talk/LXE-2783/Git,_back_to_the_future">Git, back to the future</a> - <a href="https://links.zenika.com/link/antoine-ceol-devoxx-2022">💻</a>|<a href="https://github.com/aceol/git-rewrite-history-devoxx">⌨️</a></h2>
<p>Le quickie de midi trop court, je reprends un peu de GIT avec <a href="https://twitter.com/boombaprealm">@boombaprealm</a> qui nous fait une belle démo de l'utilisation de git et ses plusieurs façons de ré-écrire l'histoire.</p>
<h3 id="notes-7">Notes</h3>
<ul>
<li>utilisation de &quot;demo magic&quot;</li>
<li>git reflog : permet d'avoir les logs même après des append</li>
<li>utilisation de rebase -i</li>
</ul>
<h3 id="conclusion-7">Conclusion</h3>
<p>Sujet sympa, peut être malgré tout un peu long au final (oui je sais je trouvais trop court l'autre session, jamais content). Par contre j'ai bien aimé la façon de présenter + l'<em>astuce</em> <code>reflog</code>.</p>
<h2 id="1830---1900-rex-tdd-avec-testcontainers">18:30 - 19:00 <a href="https://cfp.devoxx.fr/2022/talk/OCB-6800/REX:_TDD_avec_TestContainers">REX: TDD avec TestContainers</a></h2>
<p>On a commencé les sessions avec Clever Cloud, on termine avec Clever Cloud ! <a href="https://twitter.com/juuduu">@juuduu</a> nous explique l'utilisation des TestContainers.</p>
<h3 id="notes-8">Notes</h3>
<ul>
<li>Mise en place du système de facturation
<ul>
<li>zookeeper</li>
<li>pulsar</li>
<li>postgresql</li>
</ul>
</li>
<li>Testcontainers: lib java s'intégrant à JUnit et qui pilote du Docker.
<ul>
<li>voir sbt</li>
</ul>
</li>
</ul>
<h3 id="conclusion-8">Conclusion</h3>
<p>Même si le concept est intéressant, ce n'est malheureusement pas trop utile pour moi. Cela aura eu au moins le mérite de piquer ma curiosité !</p>
<h2 id="2000---2050-bof-docker--kubernetes">20:00 - 20:50 <a href="https://cfp.devoxx.fr/2022/talk/ITQ-7092/BOF_Docker_&amp;_Kubernetes">BOF Docker &amp; Kubernetes</a></h2>
<p><a href="https://twitter.com/AdrienBlind">@AdrienBlind</a>, accompagné par <a href="https://twitter.com/ndeloof">@ndeloof</a> souhaitent relancer les meetups autour de Docker &amp; Kubernetes.</p>
<p>Le sujet du BOF déviera très rapidement sur les news Docker (licences Desktop, nouvelles features, etc)</p>
<h3 id="notes-9">Notes</h3>
<ul>
<li>Ce qui est dit au BOF reste au BOF ;)</li>
</ul>
<h2 id="2100---2150-il-y-a-des-cloud-en-europe-">21:00 - 21:50 <a href="https://cfp.devoxx.fr/2022/talk/EWW-6942/Il_y_a_des_cloud_en_Europe_%3F">Il y a des cloud en Europe ?</a></h2>
<p>Animé par <a href="https://twitter.com/waxzce">@waxzce</a> &amp; <a href="https://twitter.com/LostInBrittany">@LostInBrittany</a>, une table ronde est faite autour du cloud en Europe. En gros, que faut-il en FR/EU pour que les entreprises n'aillent pas voir ailleurs ?</p>]]></content:encoded></item><item><title>Helm Chart Releaser avec GitLab CI et Parent-Child Pipelines</title><link>https://www.vrchr.fr/posts/2022/04/04/helm-chart-releaser-gitlab/</link><pubDate>Mon, 04 Apr 2022 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2022/04/04/helm-chart-releaser-gitlab/</guid><description>Générez des charts Helm à l&amp;#39;aide de la CI/CD de GitLab et la fonctionnalité de pipelines parents/enfants</description><content:encoded><![CDATA[<p>En tant qu'admin système, je gère quelques clusters Kubernetes, y déploie des applications, notamment avec Helm. J'utilise essentiellement GitLab comme plateforme de développement, avec la fonctionnalité d'Intégration et Déploiement Continue associée (GitLab CI).</p>
<h2 id="introduction">Introduction</h2>
<p>Pour livrer quelques charts Helm, j'ai cherché quelque chose de similaire à ce que fait GitHub avec les actions et le <a href="https://github.com/helm/chart-releaser-action">chart-releaser</a>. J'ai trouvé quelques projets utilisant les GitLab Pages et <a href="https://chartmuseum.com/">Chartmuseum</a>, mais rien que ne me satisfasse.</p>
<p>J'ai alors décidé d'écrire mon propre GitLab CI helm-releaser, avec les spécifications suivantes :</p>
<ul>
<li>Le dépôt des charts Helm doit avoir la même structure que ceux que l'on trouve sur GitHub (comme ceux de Bitnami, Grafana, etc.)</li>
<li>Le &quot;releaser&quot; de charts helm doit ne délivrer que les charts qui sont modifiés / mis à jour. La CI, en se déclenchant, ne doit pas tout régénérer.</li>
<li>Les charts Helm doivent être mis à disposition sur <a href="https://goharbor.io/">Harbor</a>, dépôt d'images Docker et charts Helm, que j'utilise déjà.</li>
<li>Pouvoir le déployer dans un environnement de staging Kubernetes.</li>
<li>Optionnellement, pouvoir générer une release GitLab associée.</li>
</ul>
<p>Ci-après quelques informations sur comment le chart releaser a été construit, avec les fonctionnalités de la CI GitLab et un peu de bash, comme d'habitude.</p>
<p>La fonctionnalité principale utilisée ici est de pouvoir générer des pipelines dynamiques, avec l'option &quot;parent-child pipelines&quot;. Vous trouverez plus d'information ici : <a href="https://about.gitlab.com/blog/2020/04/24/parent-child-pipelines/">https://about.gitlab.com/blog/2020/04/24/parent-child-pipelines/</a></p>
<h2 id="structure-du-dépôt-gitlab">Structure du dépôt GitLab</h2>
<p>La structure du dépôt git ressemble basiquement à cela :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="ln">1</span><span class="cl">$ tree -aL <span class="m">1</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">.
</span></span><span class="line"><span class="ln">3</span><span class="cl">├── charts
</span></span><span class="line"><span class="ln">4</span><span class="cl">├── gci-templates
</span></span><span class="line"><span class="ln">5</span><span class="cl">├── .gitlab-ci.yml
</span></span><span class="line"><span class="ln">6</span><span class="cl">└── scripts
</span></span></code></pre></div><ol>
<li>Le dossier <strong>charts</strong> contient tous les charts.</li>
<li>Le dossier <strong>gci-templates</strong> contient les templates de jobs CI/CD.</li>
<li>Le dossier <strong>scripts</strong> contient les scripts permettant la génération automatique des jobs de CI.</li>
<li>Le fichier <strong>.gitlab-ci.yml</strong> classique pour utiliser la CI GitLab.</li>
</ol>
<h2 id="publication-helm-avec-la-cicd">Publication Helm avec la CI/CD</h2>
<p>Publier un chart Helm est assez simple :</p>
<ol>
<li>On valide le linting (visibilité / syntaxe du code) du chart.</li>
<li>On vérifie que l'installation peut passer avec un <code>--dry-run</code>.</li>
<li>On package le chart et on l'envoie sur le registre de charts Harbor (ou un  autre, au choix), avec un tag <code>dev</code> (voir la partie &quot;Problèmes &amp; Contournements&quot; à ce propos).</li>
<li>On déploie une version correctement taggée sur Harbor (voir <a href="https://semver.org/">semver</a>).</li>
<li>On crée une release GitLab.</li>
</ol>
<p>Les pipelines de CI ne doivent se déclencher seulement si <code>charts/my-chart</code> est modifié. J'inclus donc un template pour éviter le copier/coller pour chaque chart que je dois générer.</p>
<p><em>Note</em> : pour ce blog post, uniquement le lint &amp; release sont affichées. Un pipeline plus complet est disponible à la fin de l'article.</p>
<p>Ce qui donne le code suivant dans le job de CI <code>.gitlab-ci.yml</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">include</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;/gci-templates/.gitlab-ci.yml&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">stages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span>- <span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span>- <span class="l">install</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span>- <span class="l">release</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nt">&#39;my-chart:lint&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">extends</span><span class="p">:</span><span class="w"> </span><span class="l">.lint</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">variables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">HELM_NAME</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;my-chart&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span>- <span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;$CI_COMMIT_TAG =~ &#34;/^$/&#34;&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">      </span><span class="nt">changes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span>- <span class="l">charts/my-chart/**/*</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="nt">&#39;my-chart:release&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">  </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">release</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">  </span><span class="nt">extends</span><span class="p">:</span><span class="w"> </span><span class="l">.release</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">  </span><span class="nt">variables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nt">HELM_NAME</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;my-chart&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">    </span>- <span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;$CI_COMMIT_TAG =~ &#34;/^$/&#34;&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">      </span><span class="nt">changes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">        </span>- <span class="l">charts/my-chart/Chart.yaml</span><span class="w">
</span></span></span></code></pre></div><p>Le template inclus ressemble à cela :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">dtzar/helm-kubectl:3.5.3</span><span class="w"> </span><span class="c"># last version using k8s 1.20</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">.lint</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="l">helm lint .</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nt">.release</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span>- <span class="l">apk add git</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span>- <span class="l">helm plugin install https://github.com/chartmuseum/helm-push</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span>- <span class="l">helm repo add --username=${DOCKER_REGISTRY_USER} --password=$(cat &#34;$DOCKER_REGISTRY_PASSWORD&#34;) ${HELM_PROJECT} https://${DOCKER_REGISTRY}/chartrepo/${HELM_PROJECT}</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span>- <span class="l">helm package .</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span>- <span class="l">helm cm-push . ${HELM_PROJECT}</span><span class="w">
</span></span></span></code></pre></div><p>OK, maintenant que le job de CI est là, on peut publier le chart... Mais comme je n'ai pas envie de récrire les tâches de CI à chaque fois, on va utiliser les pipelines générés !</p>
<h2 id="generated-pipelines">Generated Pipelines</h2>
<p>Les pipelines générés sont très simples : il s'agit d'une tâche de CI qui génère d'autres tâches. On fait cela en 2 étapes :</p>
<ol>
<li>On génère un fichier yalm qui inclut toutes les étapes de la CI qu'on doit effectuer. Ce fichier yaml peut être créé via des scripts (ici bash)</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">chart-generator</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">generate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">alpine:3.15</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">   </span>- <span class="l">./scripts/generate-pipeline.sh &gt; generated-pipeline.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">artifacts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">expire_in</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="l">hour</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span>- <span class="l">generated-pipeline.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span>- <span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;$CI_COMMIT_TAG =~ &#34;/^$/&#34;&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">changes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span>- <span class="l">charts/**/*</span><span class="w">
</span></span></span></code></pre></div><p>Le script <code>generate-pipeline.sh</code> contient le code simple précédemment vu pour générer un seul chart :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">cat <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">include: &#39;/gci-templates/.gitlab-ci.yml&#39;
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">stages:
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">  - test
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  - install
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">  - release
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">for</span> f in <span class="k">$(</span>find charts/* -maxdepth <span class="m">0</span> -type d<span class="k">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">do</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nv">CHART_VERSION</span><span class="o">=</span><span class="k">$(</span>grep <span class="s2">&#34;^version:&#34;</span> <span class="si">${</span><span class="nv">f</span><span class="si">}</span>/Chart.yaml <span class="p">|</span> cut -d<span class="s1">&#39; &#39;</span> -f2-<span class="k">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="nv">CHART_RELEASE</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">f</span><span class="p">##*/</span><span class="si">}</span><span class="s2">-</span><span class="si">${</span><span class="nv">CHART_VERSION</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  cat <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">&#39;${f##*/}:lint&#39;:
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">  stage: test
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">  extends: .lint
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">  variables:
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">    HELM_NAME: &#34;${f##*/}&#34;
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">  rules:
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="s">    - if: &#39;\$CI_COMMIT_TAG =~ &#34;/^$/&#34;&#39;
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="s">      changes:
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="s">        - ${f}/**/*
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="s">&#39;${f##*/}:release&#39;:
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="s">  stage: release
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="s">  extends: .release
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="s">  variables:
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="s">    HELM_NAME: &#34;${f##*/}&#34;
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="s">  rules:
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="s">    - if: &#39;\$CI_COMMIT_TAG =~ &#34;/^$/&#34;&#39;
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="s">      changes:
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="s">        - ${f}/Chart.yaml
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">
</span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><ol start="2">
<li>
<p>Appelons ce pipeline généré dans la prochaine étape de la CI, avec 2 mots-clés :</p>
<ul>
<li><strong>trigger</strong> : pour appeler un pipeline &quot;enfant&quot;.</li>
<li><strong>include artefact</strong> : pour appeler l'artefact précédemment généré.</li>
</ul>
</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">chart-jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">generate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">needs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span>- <span class="l">chart-generator</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">trigger</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">include</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span>- <span class="nt">artifact</span><span class="p">:</span><span class="w"> </span><span class="l">generated-pipeline.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span><span class="nt">job</span><span class="p">:</span><span class="w"> </span><span class="l">chart-generator</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">strategy</span><span class="p">:</span><span class="w"> </span><span class="l">depend</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span>- <span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;$CI_COMMIT_TAG =~ &#34;/^$/&#34;&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">changes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span>- <span class="l">charts/**/*</span><span class="w">
</span></span></span></code></pre></div><p>Avec toutes ces étapes, un pipeline enfant sera déclenché à chaque fois qu'un chart sera créé ou modifié, sans devoir recopier toutes les tâches de la CI.</p>
<h2 id="magic">Magic</h2>
<p>Maintenant que l'on a tout ce qu'il faut pour ne pas réécrire les pipelines à chaque fois, on peut pousser autant de charts que l'on veut, il suffit juste de l'inclure dans le dépôt, et la CI fera le reste !</p>
<p><img alt="GitLab Child Pipelines" class="zoomable" decoding="async" loading="lazy" src="/2022/04/2022-04-04-gitlab-pipelines.webp"></p>
<h2 id="remarques-et-ressources">Remarques et Ressources</h2>
<p>On retrouvera le &quot;Helm Chart Releaser&quot; quasiment complet ici, il suffit &quot;juste&quot; de modifier quelques variables. Bien-sûr, à tester avant utilisation ;)</p>
<p><a href="https://gitlab.com/rverchere/helm-chart-release-example">https://gitlab.com/rverchere/helm-chart-release-example</a></p>
<p>Voir aussi la section <code>release_tag</code> pour pousser les releases sur directement sur GitLab !</p>
<p>&quot;Last but not the least&quot;, il ne reste plus qu'un peu de GitOps pour déployer en prod, mais cela est une autre histoire ;)</p>
<h3 id="problèmes--contournements">Problèmes &amp; Contournements</h3>
<p>Avec Helm, il n'y a pas de notion de tag &quot;latest&quot; comme dans Docker. Pour contourner cela, on utilisera avec Harbor le tag <code>O.0.0-dev</code>. Ainsi, à chaque nouvelle release orienté <code>dev</code> dans Harbor, on utilisera ce tag.</p>]]></content:encoded></item><item><title>Comment bien foirer vos certifications Kubernetes CK{A,S}</title><link>https://www.vrchr.fr/posts/2022/01/24/comment-bien-foirer-vos-certifications-kubernetes/</link><pubDate>Mon, 24 Jan 2022 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2022/01/24/comment-bien-foirer-vos-certifications-kubernetes/</guid><description>&lt;p&gt;En début d'année 2021, j'ai tenté la Certification Kubernetes Administrator (CKA), et j'ai &lt;strong&gt;échoué&lt;/strong&gt; au premier essai.&lt;/p&gt;
&lt;p&gt;Cette année (2022), j'ai également tenté la Certification Kubernetes Security Specialist (CKS), et j'ai à nouveau &lt;strong&gt;échoué&lt;/strong&gt; au premier essai !&lt;/p&gt;</description><content:encoded><![CDATA[<p>En début d'année 2021, j'ai tenté la Certification Kubernetes Administrator (CKA), et j'ai <strong>échoué</strong> au premier essai.</p>
<p>Cette année (2022), j'ai également tenté la Certification Kubernetes Security Specialist (CKS), et j'ai à nouveau <strong>échoué</strong> au premier essai !</p>
<p><img alt="CKA/CKS Failure" class="zoomable" decoding="async" loading="lazy" src="/2022/01/2022-01-24-comment-bien-foirer-vos-certifications-kubernetes_01.webp"></p>
<p>Avec ces expériences acquises sur le &quot;fail&quot; de certifications, je vous propose quelques astuces pour mettre toutes les chances de votre côté et échouer ! Lisez alors avec attention ce qui suit, à vos risques et périls !</p>
<h2 id="avant--préparation-de-lexamen">Avant / Préparation de l'examen</h2>
<h3 id="trop-confiant">Trop confiant</h3>
<p>Nous étions quelques-uns à se motiver pour passer la certification. Quelques amis et collègues ont réussi l'examen haut la main, même certains dont je ne savais <em>même pas</em> que le sujet les intéressait. Si eux ont réussi, pourquoi pas moi ? En plus, ils n'ont jamais utilisé Kubernetes en production !</p>
<p>Premier constat : OK, ce ne sont peut être pas des experts, mais ils ont travaillé dur, bossé consciencieusement, et sont peut-être aussi habitués à passer ce genre de certifcations, pas moi ! C'est un bon point de départ si vous vous considérer suffisamment bon pour ne pas réviser et être studieux, c'est un bon début pour prévoir l'échec !</p>
<h3 id="ne-pratiquez-pas">Ne pratiquez pas</h3>
<p>La CKA et CKS sont des examens de <strong>mise en situation</strong> avec de la <strong>pratique</strong>. Entre 15 et 20 questions de type lab, pas de QCM ici. Vous devez alors être à l'aise avec la CLI et les commandes <strong>kubectl</strong>, comme par exemple:</p>
<ul>
<li>créer un <code>pod</code></li>
<li>mettre à l'échelle un <code>deployment</code></li>
<li>configurer l'<code>api-server</code></li>
<li>créer des <code>network-policies</code></li>
<li>éditer un <strong>paquet</strong> de fichiers <code>yaml</code></li>
<li>etc.</li>
</ul>
<p>Si vous ne savez pas le faire instinctivement, tant mieux, quelques recherches sur Internet pour savoir de quoi on parle devrait suffire</p>
<h3 id="napprenez-pas-vim-ni-les-raccourcis-kubectl">N'apprenez pas vim ni les raccourcis kubectl</h3>
<p>Pour être plus efficace, il existe des raccourcis sous <code>vim</code> et <code>kubectl</code>, qui vous font gagner un temps précieux (indentation yaml, forcer l'arrêt des conteneurs, etc).</p>
<p>Egalement, vous pouvez générer des manifests <code>yaml</code> avec l'option <code>--dry-run</code> pour avoir déjà une base pré-remplie.</p>
<p>Vous ne devez pas les utiliser, il ne faudrait pas que vous gagniez du temps sur la suppression de ressources, ou l'édition de fichiers.</p>
<h3 id="ne-vous-entrainez-pas-avec-killersh">Ne vous entrainez pas avec killer.sh</h3>
<p>Depuis l'été 2021, la Linux Foundation offre 2 examens blancs gratuits sur la plateforme  <a href="https://killer.sh">killer.sh</a>, qui sont plus difficiles que l'examen réel. Voir le blog post <a href="https://training.linuxfoundation.org/blog/linux-foundation-kubernetes-certifications-now-include-exam-simulator/">ici</a> pour plus de détails.</p>
<p>Ces 2 sessions sont les mêmes, et vous avez 36 heures pour chacune d'entre elles pour profiter d'aller chercher un maximum d'informations et détails sur les questions posées, et vérifier que tout fonctionne comme vous l'avez compris. Les résponses sont explicitées si vous n'y arrivez pas.</p>
<p>Ne l'utilisez surtout pas alors, vous pourriez apprendre des choses intéressantes !</p>
<h3 id="ne-faites-pas-de-bookmarks-sur-kubernetesio">Ne faites pas de bookmarks sur Kubernetes.io</h3>
<p>Dans les directives de l'examen, il est possible d'avoir un onglet supplémentaire de votre navigateur ouvert pour rechercher de la documentation et exemples sur le site officiel <a href="https//kubernetes.io">kubernetes.io</a>. Ne l'utiliez simplement pas.</p>
<p>Bon OK, ça vous démange, vous êtes quand même curieux, vous voulez y jeter un oeil. Ça me va, mais assurez-vous de ne pas avoir parcouru le site ni prévu de bookmarks sur les éléments clés de la certification (vous trouverez quelques bookmarks déjà tout prêts sur Internet, mais ne les utilisez pas vous risqueriez de réussir).</p>
<h3 id="utilisez-votre-1er-essai-le-plus-tard-possible">Utilisez votre 1er essai le plus tard possible</h3>
<p>Une fois la certification achetée, vous avez 1 an pour la passer, et 2 essais. Le 2ème essai doit être passé dans l'année. Vous pouvez alors tenter une première fois la certif, échouer, la retenter une deuxième fois, et échouer à nouveau, cela fait un beau combo !</p>
<p>Pourquoi se faire autant de mal, et simplement échouer définitivement lors du premier essai, en passant la certification le plus tard possible ? Mon conseil : planifier la date du premier essai le plus tard possible, ainsi il vous sera impossible d'utiliser votre &quot;free retake&quot;. Quoi de plus facile ?</p>
<h3 id="oubliez-que-vous-avez-souscrit-à-lexamen">Oubliez que vous avez souscrit à l'examen</h3>
<p>Cell-ci est facile: achetez un bon pour passer la CKS, travaillez sur pleins d'autres sujets qui n'ont rien à voir pendant l'année, et d'un coup recevez un mail qui vous informe qu'il ne vous reste que quelques jours pour passer la certif !</p>
<p><img alt="CKS Mail" class="zoomable" decoding="async" loading="lazy" src="/2022/01/2022-01-24-comment-bien-foirer-vos-certifications-kubernetes_04.webp"></p>
<p>C'est ce qui m'est arrivé... Seulement 1 mois pour préparer l'examen, en période de fêtes de fin d'année, avec le travail quotidien, la gestion familiale, et le 2ème essai inclus !</p>
<h3 id="ne-vous-mettez-pas-dans-des-conditions-dexamen">Ne vous mettez pas dans des conditions d'examen</h3>
<p>La CKA a été mon premier examen de la vie professionnelle après mes diplômes universitaires, &quot;quelques&quot; années plus tôt. J'ai oublié comment cela pouvait être stressant. Quelques éléments ci-après pour ne pas vous mettre en conditions de passsage d'examen :</p>
<ul>
<li>Ne dormez pas, ou peu, et faites tout ce que vous pouvez pour être le plus nerveux possible.</li>
<li>Ne nettoyez pas votre bureau / pièce, étant donné qu'on vous le demandera le jour J</li>
<li>Utilisez une webcam la plus bas de gamme possible, ainsi l'examinateur ne pourra pas vérifier votre identité</li>
<li>Prévoyez une journée de travail arrassante la veille (une bonne mise en prod bien pourrie) afin d'être le plus fatigué possible (essayez aussi de planifier l'exam tard le soir, quite à être fatigué...)</li>
<li>Si vous avez des enfants, en période COVID faites en sorte qu'ils soient malades pendant vos révisions et examen (<em>ce n'est pas une blague, ça m'est arrivé !</em>)</li>
</ul>
<p><img alt="I will fail, this is fine" class="zoomable" decoding="async" loading="lazy" src="/2022/01/2022-01-24-comment-bien-foirer-vos-certifications-kubernetes_02.webp"></p>
<h2 id="pendant-lexamen">Pendant l'examen</h2>
<p>Ca y est, on y est, c'est <em>LE</em> jour <strong>J</strong>, plus que quelques instants et vous allez le rater cet examen ! Dans le cas où vous n'êtes pas 100% sûr de vous, quelques conseils supplémentaires ci-après.</p>
<h3 id="ne-lisez-pas-les-questions-en-entier">Ne lisez pas les questions en entier</h3>
<p>Parfois, des indications vous facilitant la vie peuvent être écrites à la fin des questions. Le sachant, surtout évitez de les lire, faite ce qu'on vous demande ligne après ligne.</p>
<h3 id="nutilisez-pas-les-contextes-k8s">N'utilisez pas les contextes k8s</h3>
<p>Pour chaque question, on vous indique sur quel cluster il faudra intervenir, avec des contextes différents. Par exemple, on vous demande de corriger un déploiement sur le cluster B, mais vous étiez sur le cluster A lors de la question précédente.</p>
<p>Si vous ne changez pas de contexte, vous aurez 100% de chance de ne pas trouver le déploiement qui pose problème !</p>
<p>Cela parait <strong>trivial</strong>, mais j'ai perdu <strong>beaucoup</strong> de temps à cause du changement de contexte lors de ma première tentative CKA (<em>oui, j'étais particulièrement stressé</em>).</p>
<h3 id="ne-sauvegardez-pas-votre-travail">Ne sauvegardez pas votre travail</h3>
<p>Lorsqu'on vous demande de créer des déploiements ou tout autre ressource kubernetes, vous pouvez générer le manifest <code>yaml</code> pour l'adapter, le rejouer plus tard, si jamais vous vous êtes trompé et que vous souhaitiez corriger le tir.</p>
<p>Inutile de garder ces fichiers dans un coin, pourquoi revenir sur quelque chose qu'on pense faux, ce sera ça en plus pour louper la certif.</p>
<h3 id="prenez-votre-temps">Prenez votre temps</h3>
<p>L'examen dure deux heures. Ce n'est pas la course, prenez votre temps. En étant optimiste, l'examen comporte 15 questions, et vous devez avoir un score d'au moins 68%... Soit au minimum 11 questions justes. Sur 2 heures, cela revient à 11 minutes par question, vous êtes larges !</p>
<h3 id="lorsque-cest-terminé-arrêtez-tout-tout-de-suite">Lorsque c'est terminé, arrêtez tout, tout de suite</h3>
<p>Dans le cas ou vous n'auriez pas pris en compte le conseil précédent, que vous avez répondu à toutes les questions et donc il vous reste du temps, n'en perdez pas d'avantage ! Ne vous relisez pas, ne retestez pas vos manifests, c'est inutile. Demandez simplement à l'examinateur de clore la session le plus vite possible, et fuyez !</p>
<h2 id="fin-de-lhistoire">Fin de l'histoire</h2>
<p>Avec tous ces tips, j'espère que vous échouerez aussi bien que moi. Cela dit, si vous ne voulez pas, il suffit simplement d'appliquer l'inverse de ce que j'ai écrit précédemment ;)</p>
<p>Heureusement, après toutes mes erreurs et échecs, j'ai été plutôt contrarié et frustré. J'ai alors décidé d'utiliser mon deuxième essai la semaine suivant ma première certification : j'étais déjà dans des conditions d'examens, je sortai de plusieurs jours d'apprentissage et révisions intensifs, et je savais à peu près où j'avais échoué et j'ai pu travailler sur les sujets que je ne maitrisais pas assez ou n'étais pas assez clairs</p>
<p>Je peux alors résumer mes échecs ainsi pour chaque certification :</p>
<ol>
<li>
<p><strong>CKA</strong>: Pas assez de préparation et entrainements en CLI. Aussi une surdose de confiance en moi pour la CKA, celle-ci étant ma première certif depuis l'université</p>
</li>
<li>
<p><strong>CKS</strong>: Pas assez de préparation (j'insiste) et de temps, la période pour passer l'examen arrivait à expiration assez vite, et je savais pertinament que j'allais la rater. Mais j'ai pris ce premier essai comme une opportunité, pour me préparer le mieux que possible, pour ensuite réussir au deuxième essai.</p>
</li>
</ol>
<p>Ainsi, je peux le dire maintenant, je suis certificé CKA et CKS, et tout cette préparation en vallait le coup !</p>
<p><img alt="Badges CKA et CKS" class="zoomable" decoding="async" loading="lazy" src="/2022/01/2022-01-24-comment-bien-foirer-vos-certifications-kubernetes_03.webp"></p>
<h2 id="ce-que-jai-appris-conclusion">Ce que j'ai appris (conclusion)</h2>
<p>Evidemment, j'espère que vous ne prendrez pas mes conseils au pied de la lettre, et ferez l'inverse de ce que j'ai écrit si vous souhaitez réellement réussir vos examens CK{A,S}.</p>
<p>Ces échecs sont parfois nécessaires, nous rappelant qu'on peut se tromper. Mais apprendre de ces échecs, cela fait partie de la vie ! Ne vous sentez pas inférieurs lorsque vous lisez toutes ces success-stories sur les réseaux sociaux, tout le monde échoue un jour où l'autre !</p>
<p>Mes derniers mots seront de vous souhaiter bonne chance pour vos certifications. J'espère que vous n'échouerez pas, enfin, pas trop ;)</p>
<h2 id="post-scriptum">Post Scriptum</h2>
<p>Depuis l'écriture de cet article, et le passage réussi de la CKS, j'ai profité d'un bureau bien rangé, d'une mise en condition d'examen et d'un week-end tranquille sans les enfants pour tenter la CKAD : cette fois-ci je l'ai eu du premier coup, avec un score honorable de 98% ;)</p>
<p><img alt="Résultat CKAD" class="zoomable" decoding="async" loading="lazy" src="/2022/01/2022-01-24-comment-bien-foirer-vos-certifications-kubernetes_05.webp"></p>]]></content:encoded></item><item><title>Trouver les images vulnérables à la faille log4j avec bash et trivy dans k8s</title><link>https://www.vrchr.fr/posts/2021/12/12/log4j-k8s-images-vulnerables-trivy/</link><pubDate>Sun, 12 Dec 2021 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2021/12/12/log4j-k8s-images-vulnerables-trivy/</guid><description>CVE-2021–44228: Trouver les images vulérables à la faille log4j avec bash et trivy dans un cluster kubernetes.</description><content:encoded><![CDATA[<p>Il y a quelques jours, le 10 décembre 2021, une vulnérabilité critique a été découverte sur Apache Log4j 2, une librairie Java pour la gestion des logs. Cette faille permettant l'exécution de code à distance est surnommée Log4Shell.</p>
<p>C'est une bonne opportunité pour jouer avec bash, pour trouver au sein de cluster Kubernetes quelles images de pods sont impactés.</p>
<h2 id="lister-les-images-du-cluster">Lister les images du cluster</h2>
<p>Lister les pods est assez facile, mais lister les images est un tout petit peu plus compliqué. Nous devons jouer avec l'option <a href="https://kubernetes.io/docs/reference/kubectl/jsonpath/"><code>jsonpath</code></a> pour sélectionner l'image.</p>
<p>La commande suivante permet de lister toutes les images de pods s'exécutant dans un namespace:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl get pods -o <span class="nv">jsonpath</span><span class="o">=</span>’<span class="o">{</span>range .items<span class="o">[</span>*<span class="o">]}{</span>.spec.containers<span class="o">[</span>*<span class="o">]</span>.image<span class="o">}{</span>“ “<span class="o">}</span>’
</span></span></code></pre></div><p>Ajoutons à cela quelque lignes magiques de bash pour classer, avoir les images des sidecars et dédupliquer la sortie, cela donne le résultat suivant :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl">$ kubectl get pods -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{range .items[*]}{.spec.containers[*].image}{&#34; &#34;}&#39;</span> <span class="p">|</span> tr <span class="s2">&#34; &#34;</span> <span class="s2">&#34;\n&#34;</span> <span class="p">|</span> sort -u
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">consul:1.3.0
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">elasticsearch:5.6.13
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">grafana/grafana:5.1.0
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">jboss/keycloak:4.5.0.Final
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">jhipster/consul-config-loader:v0.3.0
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">jhipster/jhipster-console:v4.0.0
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">jhipster/jhipster-elasticsearch:v4.0.0
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">jhipster/jhipster-logstash:v4.0.0
</span></span><span class="line"><span class="ln">10</span><span class="cl">jhipster/jhipster-zipkin:v4.0.0
</span></span><span class="line"><span class="ln">11</span><span class="cl">mariadb:10.5
</span></span><span class="line"><span class="ln">12</span><span class="cl">mongo:4.0.2
</span></span><span class="line"><span class="ln">13</span><span class="cl">nextcloud:20.0.6-apache
</span></span><span class="line"><span class="ln">14</span><span class="cl">postgres:10.4
</span></span></code></pre></div><p>Maintenant que nous pouvons récupérer les images, utilisons trivy.</p>
<h2 id="trivy">Trivy</h2>
<p><a href="https://trivy.dev/">Trivy</a> est un scanner de sécurité open source développé par la société Aquasecurity. Il scanne vos images, pour y trouver toutes les vulnérabilités &quot;CVE&quot;. Ce logiciel est embarqué dans d'autres outils &quot;DevSecOps&quot;, comme VMware Harbor, GitLab, et d'autres.</p>
<p>Pour l'installer sur votre OS favori, cela se passe par la : <a href="https://aquasecurity.github.io/trivy/v0.21.2/getting-started/installation/">https://aquasecurity.github.io/trivy/v0.21.2/getting-started/installation/</a>.</p>
<p>L'utilisation de trivy est très simple. Par exemple, pour voir les vulnérabilités d'une image lancez la commande suivante :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ trivy image — severity CRITICAL elasticsearch:5.6.13
</span></span></code></pre></div><p>Le résultat parle de lui-même:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl">$ trivy image --severity CRITICAL elasticsearch:5.6.13
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">2021-12-12T21:20:06.322+0100    INFO    Detected OS: debian
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">2021-12-12T21:20:06.322+0100    INFO    Detecting Debian vulnerabilities...
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">2021-12-12T21:20:06.350+0100    INFO    Number of language-specific files: <span class="m">1</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">2021-12-12T21:20:06.350+0100    INFO    Detecting jar vulnerabilities...
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">Java <span class="o">(</span>jar<span class="o">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="o">==========</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">Total: <span class="m">6</span> <span class="o">(</span>CRITICAL: 6<span class="o">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">+-------------------------------------+------------------+----------+-------------------+---------------+---------------------------------------+
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">|</span>               LIBRARY               <span class="p">|</span> VULNERABILITY ID <span class="p">|</span> SEVERITY <span class="p">|</span> INSTALLED VERSION <span class="p">|</span> FIXED VERSION <span class="p">|</span>                 TITLE                 <span class="p">|</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">+-------------------------------------+------------------+----------+-------------------+---------------+---------------------------------------+
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">|</span> io.netty:netty                      <span class="p">|</span> CVE-2019-20444   <span class="p">|</span> CRITICAL <span class="p">|</span> 3.10.6.Final      <span class="p">|</span> 4.1.44.Final  <span class="p">|</span> netty: HTTP request smuggling         <span class="p">|</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span> --&gt;avd.aquasec.com/nvd/cve-2019-20444 <span class="p">|</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">+                                     +------------------+          +                   +               +---------------------------------------+
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span> CVE-2019-20445   <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span> netty: HttpObjectDecoder.java allows  <span class="p">|</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span> Content-Length header to accompanied  <span class="p">|</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span> by second Content-Length header       <span class="p">|</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span> --&gt;avd.aquasec.com/nvd/cve-2019-20445 <span class="p">|</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">+-------------------------------------+------------------+          +-------------------+---------------+---------------------------------------+
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="p">|</span> io.netty:netty-handler              <span class="p">|</span> CVE-2019-20444   <span class="p">|</span>          <span class="p">|</span> 4.1.13.Final      <span class="p">|</span> 4.1.44        <span class="p">|</span> netty: HTTP request smuggling         <span class="p">|</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span> --&gt;avd.aquasec.com/nvd/cve-2019-20444 <span class="p">|</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">+                                     +------------------+          +                   +---------------+---------------------------------------+
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span> CVE-2019-20445   <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span> 4.1.45        <span class="p">|</span> netty: HttpObjectDecoder.java allows  <span class="p">|</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span> Content-Length header to accompanied  <span class="p">|</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span> by second Content-Length header       <span class="p">|</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span> --&gt;avd.aquasec.com/nvd/cve-2019-20445 <span class="p">|</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">+-------------------------------------+------------------+          +-------------------+---------------+---------------------------------------+
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="p">|</span> org.apache.logging.log4j:log4j-api  <span class="p">|</span> CVE-2021-44228   <span class="p">|</span>          <span class="p">|</span> 2.11.1            <span class="p">|</span> 2.15.0        <span class="p">|</span> log4j-core: Remote code execution     <span class="p">|</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span> in Log4j 2.x when logs contain        <span class="p">|</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span> an attacker-controlled...             <span class="p">|</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span> --&gt;avd.aquasec.com/nvd/cve-2021-44228 <span class="p">|</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">+-------------------------------------+                  +          +                   +               +                                       +
</span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="p">|</span> org.apache.logging.log4j:log4j-core <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span>                                       <span class="p">|</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span>                                       <span class="p">|</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span>                                       <span class="p">|</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="p">|</span>                                     <span class="p">|</span>                  <span class="p">|</span>          <span class="p">|</span>                   <span class="p">|</span>               <span class="p">|</span>                                       <span class="p">|</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">+-------------------------------------+------------------+----------+-------------------+---------------+---------------------------------------+
</span></span></code></pre></div><p>On voit que la vulnérabilité CVE-2021–44228  est présente, nous devons patcher ou mettre à jour dès que possible !</p>
<h2 id="bash--trivy">Bash &amp; Trivy</h2>
<p><em>Edit (15/12/2021)</em> : Il existe un plug-in tout fait pour cela ! Voir ici: <a href="https://github.com/aquasecurity/trivy-plugin-kubectl">https://github.com/aquasecurity/trivy-plugin-kubectl</a></p>
<p>Ok, maintenant que nous avons la liste des images dans notre cluster, que nous savons scanner une image, allons plus loin, et bouclons !</p>
<p>Voici un script bash permettant cela juste ci-dessous. Amusez-vous avec ! Notez que dans l'exemple nous listons les images du namespace courant. Vous pouvez l'améliorer au besoin.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nv">RED</span><span class="o">=</span><span class="s1">&#39;\033[0;31m&#39;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nv">NC</span><span class="o">=</span><span class="s1">&#39;\033[0m&#39;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nv">OLDIFS</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$IFS</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nv">IFS</span><span class="o">=</span><span class="s1">$&#39;\n&#39;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nv">VULN</span><span class="o">=</span><span class="nv">$1</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># $1 arg is the CVE number to check</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">if</span> <span class="o">[</span> -z <span class="nv">$1</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nb">echo</span> -e <span class="s2">&#34;usage: </span><span class="nv">$0</span><span class="s2"> CVE-NUMBER (i.e: &#39;./k8s_vuln.sh CVE-2021-44228&#39;)&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nb">exit</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># Check command existence before using it</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="k">if</span> ! <span class="nb">command</span> -v trivy <span class="p">&amp;</span>&gt; /dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;trivy not found, please install it&#34;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="nb">exit</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="k">if</span> ! <span class="nb">command</span> -v kubectl <span class="p">&amp;</span>&gt; /dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;kubectl not found, please install it&#34;</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="nb">exit</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"># CVE-2021-44228</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Scanning </span><span class="nv">$1</span><span class="s2">...&#34;</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="nv">namespaces</span><span class="o">=</span><span class="sb">`</span>kubectl get ns <span class="p">|</span> cut -d<span class="s1">&#39; &#39;</span> -f <span class="m">1</span> <span class="p">|</span> tail -n+2<span class="sb">`</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="k">for</span> ns in <span class="si">${</span><span class="nv">namespaces</span><span class="si">}</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;- scanning in namespace </span><span class="si">${</span><span class="nv">ns</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">  <span class="nv">imgs</span><span class="o">=</span><span class="sb">`</span>kubectl get pods,deployments,daemonsets,statefulsets,jobs,cronjobs -n <span class="si">${</span><span class="nv">ns</span><span class="si">}</span> -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{range .items[*]}{.spec.containers[*].image}{&#34; &#34;}&#39;</span> <span class="p">|</span> tr <span class="s2">&#34; &#34;</span> <span class="s2">&#34;\n&#34;</span> <span class="p">|</span> sort -u<span class="sb">`</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">  <span class="k">for</span> img in <span class="si">${</span><span class="nv">imgs</span><span class="si">}</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;  scanning </span><span class="si">${</span><span class="nv">img</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">    <span class="nv">result</span><span class="o">=</span><span class="sb">`</span>trivy -q image --light --no-progress --severity CRITICAL <span class="si">${</span><span class="nv">img</span><span class="si">}</span><span class="sb">`</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="k">if</span> <span class="nb">echo</span> <span class="si">${</span><span class="nv">result</span><span class="si">}</span> <span class="p">|</span> grep -q <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">      <span class="nb">echo</span> -e <span class="s2">&#34;  </span><span class="si">${</span><span class="nv">RED</span><span class="si">}${</span><span class="nv">img</span><span class="si">}</span><span class="s2"> is vulnerable, please patch!</span><span class="si">${</span><span class="nv">NC</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">
</span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="nv">IFS</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$OLDIFS</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p><img alt="Bash script k8s vulnerability" class="zoomable" decoding="async" loading="lazy" src="/2021/12/2021-12-12-check_vuln.webp"></p>
<h2 id="etapes-suivantes">Etapes suivantes</h2>
<p>Ceci était un rapide exemple d'utilisation bash, mais vous pouvez utiliser des outils de sécurité plus avancé pour être alerté de telles failles, comme <a href="https://sysdig.com/blog/cve-critical-vulnerability-log4j/">Sysdig Secure</a>, <a href="https://snyk.io/blog/log4j-rce-log4shell-vulnerability-cve-2021-4428/">Snyk</a> ou d'autres !</p>
<h2 id="references">References</h2>
<ul>
<li>Docker blog post: <a href="https://www.docker.com/blog/apache-log4j-2-cve-2021-44228/">https://www.docker.com/blog/apache-log4j-2-cve-2021-44228/</a></li>
<li>Apache Log4j : <a href="https://logging.apache.org/log4j/2.x/security.html">https://logging.apache.org/log4j/2.x/security.html</a></li>
</ul>]]></content:encoded></item><item><title>Conférence Open Source Experience 2021</title><link>https://www.vrchr.fr/posts/2021/11/10/opensourcexp/</link><pubDate>Wed, 10 Nov 2021 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2021/11/10/opensourcexp/</guid><description>&lt;p&gt;A l'occasion de l'Open Source Experience 2021, j'ai eu la chance de pouvoir présenter un sujet non technique : &lt;em&gt;Comment contribuer à l'Open Source (ou pas) sans être éditeur de solutions Open Source&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Cet exercice de 20 minutes était ma première vraie conférence en tant que speaker, avec un accueil plutôt positif du public. J'espère que cela ne sera pas la dernière ;)&lt;/p&gt;</description><content:encoded><![CDATA[<p>A l'occasion de l'Open Source Experience 2021, j'ai eu la chance de pouvoir présenter un sujet non technique : <em>Comment contribuer à l'Open Source (ou pas) sans être éditeur de solutions Open Source</em></p>
<p>Cet exercice de 20 minutes était ma première vraie conférence en tant que speaker, avec un accueil plutôt positif du public. J'espère que cela ne sera pas la dernière ;)</p>
<p>Je retiens 3 points sur cette expérience :</p>
<ol>
<li>le sujet était assez simple à présenter, car je n'ai parlé que de mon expérience, sans aucune démo technique à faire</li>
<li>20 minutes c'est vraiment court, il faut aller à l'essentiel. Pour de sujets plus techniques, je recommande au moins 40 minutes</li>
<li>La préparation d'un talk est longue, même si le sujet est maitrisé, et les feedbacks sont toujours bons à prendre pour s'améliorer</li>
</ol>
<p>Vous trouverez le replay de la présentation <a href="https://youtu.be/tkN_1tHcNNQ">ici</a>. Malheureusement il n'a été enregistré qu'en audio, vous n'y verrez pas ma tête ;)</p>
<p>Le support de présentation est quand à lui disponible <a href="https://presentations.verchere.fr/Opensource_Experience_2021/">ici</a></p>]]></content:encoded></item><item><title>DNS-01 challenge: Certificats wildcard TLS sur Kubernetes</title><link>https://www.vrchr.fr/posts/2021/09/23/dns-01-challenge-wildcard-cert-kubernetes/</link><pubDate>Thu, 23 Sep 2021 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2021/09/23/dns-01-challenge-wildcard-cert-kubernetes/</guid><description>DNS-01 challenge: Générez des certificats wildcard TLS sur kubernetes avec cert-manager et let&amp;#39;s encrypt</description><content:encoded><![CDATA[<p>Lorsqu'on déploie des applications sur un cluster kubernetes, celles-ci sont souvent exposées à l'extérieur en HTTPS via un ingress controller, généralement en utilisant cert-manager pour gérer les certificats TLS avec Let's Encrypt.</p>
<p>La demande de certificat TLS est généralement faite avec le challenge <strong>HTTP-01</strong>, qui vérifie que l'on peut accéder à du contenu sur une URL spécifique.</p>
<p>Cependant, d'autres types de challenges existent, notamment le challenge <strong>DNS-01</strong> : Let's Encrypt vérifier une entrée DNS spécifiques, pour s'assurer que le domaine sur lequel nous demandons un certificat nous appartien bien. Cela peut être utile dans plusieurs cas :</p>
<ul>
<li>Les applications ne sont pas exposées sur Internet (Let's Encrypt ne peut alors pas se connecter sur l'URL)</li>
<li>Le certificats wildcard, car il n'y a pas d'URL par défaut ou héberger la preuve de certificat</li>
</ul>
<p>On peut retrouver plus d'informations sur les différents types de challenge ici : <a href="https://letsencrypt.org/docs/challenge-types/">https://letsencrypt.org/docs/challenge-types/</a></p>
<p>Pour demander un certificat de cette manière dans un cluster kubernetes, on aura besoin de mettre à jour automatiquement les entrées DNS associées à l'application chez le cloud provider. Seulement les providers &quot;majeurs&quot; sont nativement supportés, mais on peut étendre ce fonctionnement avec des webhooks.</p>
<p>Dans cet article, nous verrons comment paramétrer un cluster kubernetes, configurer cert-manager pour gérer des challenges DNS-01 avec des certificats Wildcards, et déployer une application utilisatn ces certificats.</p>
<h2 id="paramétrage-et-pré-requis">Paramétrage et Pré-requis</h2>
<p>Pour la démo, on utilise les outils et plateformes suivants :</p>
<ul>
<li>Cluster Kubernetes Kapsule de chez Scaleway</li>
<li>Serveur DNS de chez Scaleway toujours, car on peut le piloter avec les cert-manager webhooks</li>
<li>Terraform pour facilement tout mettre en palce</li>
<li>k8s Lens pour facilement voir les ressourcs déployées</li>
</ul>
<p>L'article ne couvre pas l'installation de Terraform &amp; autres outils kubectl, helm and co.</p>
<h2 id="provisionnement-du-cluster">Provisionnement du cluster</h2>
<p>Avec Terraform, on provisionne un cluster Kapsule simple. Il suffit juste d'utilser les ressources <code>scaleway_k8s_cluster</code> et <code>scaleway_k8s_pool</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-tf" data-lang="tf"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nx">terraform</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">required_providers</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">scaleway</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">      <span class="nx">source</span> <span class="o">=</span> <span class="s2">&#34;scaleway/scaleway&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="nx">version</span> <span class="o">=</span> <span class="s2">&#34;2.1.0&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nx">required_version</span> <span class="o">=</span> <span class="s2">&#34;&gt;= 0.13&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="kr">provider</span> <span class="s2">&#34;scaleway&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nx">zone</span>            <span class="o">=</span> <span class="s2">&#34;fr-par-1&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">region</span>          <span class="o">=</span> <span class="s2">&#34;fr-par&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="kr">resource</span> <span class="s2">&#34;scaleway_k8s_cluster&#34;</span> <span class="s2">&#34;k8s_cluster&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="nx">name</span> <span class="o">=</span> <span class="nb">var</span><span class="p">.</span><span class="nx">k8s_cluster_name</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="nx">version</span> <span class="o">=</span> <span class="s2">&#34;1.22.2&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="nx">cni</span> <span class="o">=</span> <span class="s2">&#34;cilium&#34;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="kr">resource</span> <span class="s2">&#34;scaleway_k8s_pool&#34;</span> <span class="s2">&#34;k8s_pool&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="nx">cluster_id</span> <span class="o">=</span> <span class="nx">scaleway_k8s_cluster</span><span class="p">.</span><span class="nx">k8s_cluster</span><span class="p">.</span><span class="nx">id</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="nx">name</span> <span class="o">=</span> <span class="nb">var</span><span class="p">.</span><span class="nx">k8s_pool_name</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="nx">node_type</span> <span class="o">=</span> <span class="s2">&#34;DEV1-M&#34;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="nx">size</span> <span class="o">=</span> <span class="m">2</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="nx">autohealing</span> <span class="o">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="kr">resource</span> <span class="s2">&#34;local_file&#34;</span> <span class="s2">&#34;kubeconfig&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">  <span class="nx">content</span> <span class="o">=</span> <span class="nx">scaleway_k8s_cluster</span><span class="p">.</span><span class="nx">k8s_cluster</span><span class="p">.</span><span class="nx">kubeconfig</span><span class="p">[</span><span class="m">0</span><span class="p">].</span><span class="nx">config_file</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">  <span class="nx">filename</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nx">path</span><span class="p">.</span><span class="nb">module</span><span class="si">}</span><span class="s2">/</span><span class="si">${</span><span class="nx">scaleway_k8s_cluster</span><span class="p">.</span><span class="nx">k8s_cluster</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="s2">-kubeconfig&#34;</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">  <span class="nx">file_permission</span> <span class="o">=</span> <span class="s2">&#34;0600&#34;</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Maintenant on peut provisionner le cluster :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ terraform init
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ terraform plan -out k8s_scaleway.out
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ terraform apply k8s_scaleway.out
</span></span></code></pre></div><p>Après quelques minutes d'attente, le cluster est prêt et dispo, avec Nginx comme ingress controller, un load-balancer, et cert-manager de configuré. On a également un fichier kubeconfig à disposition pour contacter le cluster !</p>
<h2 id="paramétrage-dns">Paramétrage DNS</h2>
<p>On peut maintenant configurer l'entrée DNS pour faire le lien avec l'IP du load-balancer. On pourrait faire cela automatiquement, mais les ressources Terraform n'existent pas encore, c'est prévu pour une prochaine release, voir <a href="https://github.com/scaleway/terraform-provider-scaleway/blob/master/docs/resources/domain_record.md">ici</a>.</p>
<p>Pour ce setup, le domaine utilisé est scw.vrchr.fr, et donc chaque URL se terminant ainsi sera automatiquement dirigée vers l'IP du load-balancer du cluster kubernetes préalablement installé.</p>
<p><img alt="scw.vrch.fr" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-23-dns-scaleway.webp"></p>
<p><em>Note :</em> le cluster de démo n'existe plus à l'heure actuelle, pas la peine d'essayer de le joindre, cela n'a été fait que le temps de l'article ;)</p>
<h2 id="certificat-wildcard-avec-dns-01">Certificat wildcard avec DNS-01</h2>
<h3 id="webhook-scaleway">Webhook Scaleway</h3>
<p>Pour rapppel, pour certains cloud providers, on doit utiliser les webhooks cert-manager afin de piloter les entrées DNS. C'est le cas pour Scaleway, alors installons ce qu'il faut :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ git clone https://github.com/scaleway/cert-manager-webhook-scaleway.git
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ <span class="nb">cd</span> cert-manager-webhook-scaleway
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ helm install scaleway-webhook deploy/scaleway-webhook --set secret.accessKey<span class="o">=</span>changeme --set secret.secretKey<span class="o">=</span>changeme --set certManager.serviceAccountName<span class="o">=</span>jetstack-cert-manager --namespace<span class="o">=</span>cert-manager
</span></span></code></pre></div><p>Attention au serviceAccountName qui doit être le même que celui créé avec le chart helm cert-manager.</p>
<p>On peut maintenant voir les ressources déployées :</p>
<p><img alt="cert-manager deployments" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-23-cert-manager.webp"></p>
<p><img alt="cert-manager CRDs" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-23-cert-manager-crd.webp"></p>
<h3 id="issuer-and-certificates">Issuer and Certificates</h3>
<p>Maintenant, on va créer un &quot;Certificate Issuer&quot;, qui sera responsable des demandes de certificats TLS. Il s'agit d'une Custom Resource avec la propriété &quot;dns01&quot;. Dans l'exemple on créé un ClusterIssuer plutôt qu'un Issuer, afin de gérer les demandes de certificats sur l'ensemble du cluster :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIssuer</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">scaleway-issuer-prod</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">acme</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">email</span><span class="p">:</span><span class="w"> </span><span class="l">name@company.com</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">server</span><span class="p">:</span><span class="w"> </span><span class="l">https://acme-v02.api.letsencrypt.org/directory</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">privateKeySecretRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">scaleway-private-key-secret</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">solvers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span>- <span class="nt">dns01</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="nt">webhook</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">          </span><span class="nt">groupName</span><span class="p">:</span><span class="w"> </span><span class="l">acme.scaleway.com</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">          </span><span class="nt">solverName</span><span class="p">:</span><span class="w"> </span><span class="l">scaleway</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl apply -f cert_issuer.yaml
</span></span></code></pre></div><p>On peut rajouter un issuer pour les certificats de type staging si besoin.</p>
<p>Maintenant, créons une demande de certificat ! La demande sera faite pour &quot;*.scw.vrchr.fr&quot;, qui sera enregistrée dans un secret nommé &quot;wildcard-scw-vrchr-fr-tls” :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Certificate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wildcard-scw-vrchr-fr-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">dnsNames</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span>- <span class="s2">&#34;*.scw.vrchr.fr&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">issuerRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">scaleway-issuer-prod</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIssuer</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">wildcard-scw-vrchr-fr-tls</span><span class="w">
</span></span></span></code></pre></div><p>Que se passe-t'il maintenant ?</p>
<ul>
<li>Cert-manager détecte une demande de certificat, et génère une demande vers les serveurs Let’s Encrypt</li>
<li>Cert-manager utilise le webhook pour créer une entrée DNS TXT DNS : _acme-challenge.scw.vrchr.fr</li>
<li>Les serveurs Let's Encrypt vont vérifier cette entrée DNS, et valident la requpete. Il faut être patient, le temps de la propagation DNS qui peut être longue</li>
<li>Une fois la demande validée, le certificat est créé dans un secret disponible dans le cluster !</li>
</ul>
<p><img alt="TLS Certificate stored in secrets" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-23-secrets-wildcard.webp"></p>
<h3 id="deploiement-dapplication">Deploiement d'application</h3>
<p>OK ! Maintenant qu'on a un certificat wildcard généré automatiquement, utilisons-le sur des applications.</p>
<p>On créé un déploiement simple, on l'expose via un service, et on créé une règle d'ingress. La partie importante de la configuration est dans l'ingress, qui spécifie quel certificat TLS utilisé. Pas besoin d'annotations particulières pour géré le certificat automatique, car il est déjà créé et géré ailleurs.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span>- <span class="s2">&#34;*.scw.vrchr.fr&#34;</span><span class="w"> 
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">wildcard-scw-vrchr-fr-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span></code></pre></div><p>Comme on a créé un certificat wildcard, chaque FQDN qui matchera le wildcard sera valide.</p>
<p>Voici l'exemple d'un déploiement complet de l'application  <a href="https://hub.docker.com/r/jpetazzo/webcolor">jpetazzo/webcolor</a>  qui expose différentes couleurs.</p>
<p><strong>Exemple de déploiement &amp; service</strong>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">green</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">green</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">green</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">jpetazzo/webcolor</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">webcolor</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">green</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">  </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">    </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">green</span><span class="w">
</span></span></span></code></pre></div><p><strong>Ingress</strong>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">color-ingress</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.class</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span>- <span class="s2">&#34;*.scw.vrchr.fr&#34;</span><span class="w"> 
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">wildcard-scw-vrchr-fr-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">green.scw.vrchr.fr</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">      </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">        </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">Prefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">        </span><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">          </span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">green</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">            </span><span class="nt">port</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">              </span><span class="nt">number</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">  </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">purple.scw.vrchr.fr</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">    </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">      </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">      </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">        </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">Prefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">        </span><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">          </span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">purple</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">            </span><span class="nt">port</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">              </span><span class="nt">number</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">  </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">yellow.scw.vrchr.fr</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">    </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="w">      </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="w">      </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="w">        </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">Prefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="w">        </span><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="w">          </span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">yellow</span><span class="w">
</span></span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="w">            </span><span class="nt">port</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="w">              </span><span class="nt">number</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl apply -f dep_green.yaml
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ kubectl apply -f wildcard_tls_ingress.yaml
</span></span></code></pre></div><p>Maintenant, accédons à <a href="https://green.scw.vrchr.fr">https://green.scw.vrchr.fr</a>, qui devrait présenter le certificat wildcard !</p>
<p><img alt="Green Page" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-23-dns-green.webp"></p>
<p><img alt="Yellow Page" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-23-dns-yellow.webp"></p>
<p><img alt="Wildcard Certificate" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-23-dns-cert.webp"></p>
<h2 id="conclusion-et-remarques">Conclusion et remarques</h2>
<p>Dans cet article on a pu :</p>
<ul>
<li>Déployer un cluster kubernetes sur Scaleway avec Terraform</li>
<li>Déployer Cert Manager &amp; Nginx via Helm &amp; Terraform</li>
<li>Déployer les DNS Webhook Scaleway</li>
<li>Générer des certificats wildcard avec le protocole DNS-01 via les webhooks</li>
<li>Déployer une application dans un sous-domaine, présentant le certificats wildcard</li>
</ul>
<p>Maintenant on peut déployer autant d'application dans le sous-domaine que l'on veut, en utilisant toujours le même certificat. Pour faciliter l'exercice, ici tout a été fait dans un même namespace, ne pas hésiter à lire la doc de cert-manager pour partager les certificats entre plusieurs namespaces.</p>
<p>Enfin on retrouvera le code d'exemple de l'articile ici : <a href="https://gitlab.com/rverchere/vrchr-k8s-dns-demo">https://gitlab.com/rverchere/vrchr-k8s-dns-demo</a></p>
<p>Et n'oublions pas : &quot;It's always DNS&quot; !</p>
<p><img alt="DNS Haiku" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-23-dns-01.webp"></p>
<h2 id="références">Références</h2>
<p>Quelques ressources très utiles pour comprendre les concepts utilisés dans l'articule :</p>
<ul>
<li><a href="https://particule.io/blog/scaleway-cert-manager/">https://particule.io/blog/scaleway-cert-manager/</a></li>
<li><a href="https://www.youtube.com/watch?v=OvxSk5YVvII&amp;t=1366s">https://www.youtube.com/watch?v=OvxSk5YVvII&t=1366s</a></li>
<li><a href="https://cert-manager.io">https://cert-manager.io</a></li>
<li><a href="https://registry.terraform.io/providers/scaleway/scaleway/latest">https://registry.terraform.io/providers/scaleway/scaleway/latest</a></li>
</ul>]]></content:encoded></item><item><title>Créer des slides depuis n'importe où avec Marp, GitLab Pages et Gitpod</title><link>https://www.vrchr.fr/posts/2021/09/09/marp-gitpod/</link><pubDate>Thu, 09 Sep 2021 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2021/09/09/marp-gitpod/</guid><description>Créer des slides depuis n&amp;#39;importe où avec Marp, GitLab Pages et Gitpod.</description><content:encoded><![CDATA[<p>Avec mon job actuel, je fais quelques présentations publiques. Comme la plupart des gens aiment travailler à distance, nous allons voir aujourd'hui comment écrire et publier des slides depuis n'importe où, pour n'importe qui !</p>
<p>Ci-après je vous explique comment vous pouvez écrire des slides en Markdown, puis générer un site web statique automatiquement avec les outils suivants :</p>
<ul>
<li><a href="https://marpit.marp.app/">Marp</a>: Un écosystème pour générer des présentations en Markdown.</li>
<li><a href="https://www.gitpod.io/">Gitpod</a>: Un éditeur de code en ligne, de type &quot;vscode-as-a-service&quot;, avec des workspaces automatiquement provisionnés.</li>
<li><a href="https://docs.gitlab.com/ee/user/project/pages/">GitLab Pages</a>: Un job de CI/CD permettant de générer un site statique avec GitLab</li>
</ul>
<p>Pour ce faire, vous aurez besoin d'un compte GitLab et Gitpod.</p>
<h2 id="introduction-marp">Introduction Marp</h2>
<p>Marp est un &quot;écosystème de présentation Markdown&quot;, qui vous permet de vous concentrer sur le fond de vos présentations en utilisant la syntaxe Markdown, en fournissant des outils pour les convertir facilement en PDF, HTML, etc. Vous pouvez utiliser des styles prédéfinis, les customiser ou bien créer vos propres styles pour la génération des documents.</p>
<p>Par exemple, le code suivant génèrera les slides affichées sur le côté.</p>
<p><img alt="Marp Markdown avec une prévisualisation dans VS Code" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-09-marp-01.webp"></p>
<p>Je ne vais pas parler de comment utiliser Marp pour créer de belles présentations dans cet article, seulement comment utiliser les outils de conversion efficacement.</p>
<p>Vous pouvez convertir du Markdown avec les différents outils Marp : CLI (marp-cli), Extension VS Code (marp-vscode).</p>
<p>Pour utiliser l'extension VS Code, il suffit simplement de l'installer, CQFD :</p>
<p><img alt="Marp for VS Code extension" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-09-marp-02.webp"></p>
<p>Maintenant, vous pouvez directement exporter les slides depuis votre éditeur favori.</p>
<p><img alt="Marp export" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-09-marp-03.webp"></p>
<p>Vous pouvez également utiliser la CLI pour automatiser le processus de build, ce que nous allons voir juste après.</p>
<h2 id="gitlab-et-gitpod-pour-créer-les-slides">GitLab et Gitpod pour créer les slides</h2>
<p>OK, maintenant que vous savez comment créer des présentations depuis du Markdown, nous allons utiliser GitLab pour y stocker notre code Markdown, mais aussi utiliser Gitpod pour avoir un environnement avec tout ce qu'il faut pour générer les slides.</p>
<p>Concernant GitLab, créez juste un dépot, commit et poussez vos slides Markdown, et tout le reste (images, CSS, etc.).</p>
<p>Maintenant, nous allons créer une instance Gitpod. Grâce à son intégration dans GitLab depuis la version 13.4, vous n'avez qu'à clicliquercker sur le menu d'édition et choisir &quot;Gitpod&quot;.</p>
<p>Pour avoir une intégration complète Marp, créez un fichier <code>.gitpod.yml</code> à la racine du projet avec les informations ci-après, ainsi le workspace Gitpod chargera automatiquement les extensions vscode et CLI (utilisant l'image docker officielle) :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">tasks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Add marp-cli container</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">init</span><span class="p">:</span><span class="w"> </span><span class="l">docker pull marpteam/marp-cli</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Configure aliases</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="sd">      alias marp=&#39;docker run --rm --init -v $PWD:/home/marp/app/ -e LANG=$LANG -e MARP_USER=&#34;$(id -u):$(id -g)&#34; marpteam/marp-cli&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">vscode</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">extensions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">   </span><span class="c"># Fix version to be compatible with current Gitpod workspace</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">   </span>- <span class="l">marp-team.marp-vscode@1.3.0</span><span class="w">
</span></span></span></code></pre></div><p><img alt="GitLab Gitpod IDE" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-09-marp-04.webp"></p>
<p>Cela provisionnera un workspace avec l'extension Marp, mais aussi la commande <code>marp</code> dans le terminal. Vous devrez vous logger et accepter le lien entre Gitpod et GitLab.</p>
<p><img alt="Gitpod Workspace avec Marp" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-09-marp-05.webp"></p>
<p>Vous pouvez utiliser la commande <code>marp</code> depuis le terminal, qui est un alias vers le conteneur Docker :</p>
<p><img alt="Gitpod Terminal" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-09-marp-06.webp"></p>
<p><em>Note</em>: Attention avec les extensions, vous devrez utilisez un version compatible avec vs code embarqué de Gitpod pour éviter ce genre de message :</p>
<p><img alt="Version incompatible" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-09-marp-07.webp"></p>
<h2 id="publication-automatique-avec-les-gitlab-pages">Publication automatique avec les GitLab Pages</h2>
<p>A ce stade, vous pouvez lancer automatiquement une instance Gitpod depuis votre dépôt GitLab, y stocker vos présentations Markdown et les générer au format que vous souhaitez via l'extension VS Code ou la CLI... Mais il reste une tâche manuelle : la génération et la publication des slides.</p>
<p>Nous allons régler celà grâce à GitLab CI/CD et les GitLab Pages.</p>
<p>Tout d'abord, créez un pipeline qui va générer les slides en HTML :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">stages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span>- <span class="l">build</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">generate-deck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">build</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">image</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">marpteam/marp-cli</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">script </span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span>- <span class="l">mkdir build/</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span>- <span class="l">export MARP_USER=&#34;$(id -u):$(id -g)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span>- <span class="l">/home/marp/.cli/docker-entrypoint -I . -o build/</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span>- <span class="l">/home/marp/.cli/docker-entrypoint -I . --pdf -o build/</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="nt">artifacts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;build/&#34;</span><span class="w">
</span></span></span></code></pre></div><p><em>Note</em>: vous devrez surcharger l'entrypoint de l'image Docker pour pouvoir lancer la commande depuis la CI.</p>
<p>Cela génèrera les fichiers HTML et PDF dan le dossier <code>build/</code>, et les rendra accessible en tant qu'artefact.</p>
<p>Ensuite, vous pouvez activer les GitLab Pages pour exposer ces pages HTML directement sur Internet :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">stages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span>- <span class="l">build</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="l">deploy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nt">pages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">deploy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="c"># Just move the pages to the public dir</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="l">mv build public</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">artifacts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span>- <span class="l">public</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">dependencies</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span>- <span class="l">generate-deck</span><span class="w">
</span></span></span></code></pre></div><p><img alt="Pipeline CI/CD" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-09-marp-08.webp"></p>
<p>Attendez quelques minutes... et voilà ! Vous pouvez accéder aux pages GitLab qui hébergent vos slides en HTML depuis n'importe où !</p>
<p><img alt="Demo Page Web" class="zoomable" decoding="async" loading="lazy" src="/2021/09/2021-09-09-marp-09.webp"></p>
<h2 id="conclusion">Conclusion</h2>
<p>Nous avons vu un démo rapide sur comment utiliser une suite d'outils complète pour écrire des présentations avec Marp, utiliser Gitpod pour avoir un éditeur de texte accessible facilement, et les outils GitLab CI/CD et Pages pour automatiser les process de génération et publication.</p>
<p>Vous pouvez accéder au code de démo <a href="https://gitlab.com/rverchere/marp-gitpod-demo">ici</a>. N'hésitez pas à le forker et expérimenter !</p>
<h2 id="références">Références</h2>
<ul>
<li><a href="https://gitlab.com/k33g_org/k33g_org.gitlab.io/-/issues/82">Set up a GitLab.com Civo Kubernetes integration with GitPod (from everywhere)</a></li>
</ul>]]></content:encoded></item></channel></rss>