<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Nginx on Zwindler's Reflection</title><link>https://blog.zwindler.fr/tags/nginx/</link><description>Recent content in Nginx on Zwindler's Reflection</description><generator>Hugo -- gohugo.io</generator><language>fr</language><copyright>Licensed under CC BY-SA 4.0</copyright><lastBuildDate>Sat, 21 Feb 2026 10:00:00 +0200</lastBuildDate><atom:link href="https://blog.zwindler.fr/tags/nginx/index.xml" rel="self" type="application/rss+xml"/><item><title>De F à A+ sur HTTP Observatory : sécuriser les headers de mon blog Hugo</title><link>https://blog.zwindler.fr/2026/02/20/securite-headers-http-observatory-hugo/</link><pubDate>Sat, 21 Feb 2026 10:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2026/02/20/securite-headers-http-observatory-hugo/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/02/observatory-a-plus.webp" alt="Featured image of post De F à A+ sur HTTP Observatory : sécuriser les headers de mon blog Hugo" /&gt;&lt;p&gt;Après &lt;a class="link" href="https://blog.zwindler.fr/2026/02/19/optimisation-webperf-avif-precompression/" &gt;l&amp;rsquo;optimisation des performances du blog&lt;/a&gt; (AVIF, pré-compression), j&amp;rsquo;étais super content de moi. 🥳&lt;/p&gt;
&lt;p&gt;Mais c&amp;rsquo;était sans compter sur Antoine Caron, qui est venu (à très juste titre) me chatouiller sur un autre aspect que j&amp;rsquo;avais ignoré, le rapport Mozilla Observatory&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/antoine-mozilla-observatory.avif"
loading="lazy"
alt="https://bsky.app/profile/slashgear.dev/post/3mfc62ckw3s2g"
&gt;&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai donc lancé un scan &lt;a class="link" href="https://developer.mozilla.org/en-US/observatory" target="_blank" rel="noopener"
&gt;Mozilla HTTP Observatory&lt;/a&gt; sur mon blog et le résultat était : &lt;strong&gt;F&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Cool, je peux pas faire pire.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/observatory-initial.avif"
loading="lazy"
alt="Le scan initial : F avec 0/100"
&gt;&lt;/p&gt;
&lt;h2 id="le-déclic"&gt;Le déclic
&lt;/h2&gt;&lt;p&gt;En relisant &lt;a class="link" href="https://codeka.io/2026/02/20/optimiser-les-perfs-et-la-s%C3%A9curit%C3%A9-dun-site-hugo/" target="_blank" rel="noopener"
&gt;l&amp;rsquo;article de Julien sur codeka.io&lt;/a&gt;, qui a aussi parlé avec Antoine, a aussi un blog statique avec Hugo, MAIS qui lui avait fait le taf, j&amp;rsquo;ai réalisé que c&amp;rsquo;était à la fois &lt;strong&gt;important et peut-être pas si compliqué&lt;/strong&gt; à corriger.&lt;/p&gt;
&lt;p&gt;Julien y détaille les headers de sécurité à ajouter sur un site Hugo, avec des exemples pour Caddy. Moi je suis sur nginx, mais le principe est le même.&lt;/p&gt;
&lt;p&gt;Note : si ça vous intéresse, les liens des épisodes précédents sur l&amp;rsquo;infra du blog sont ici :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2019/12/24/ca-bouge-pas-mal-sur-le-blog/" &gt;Ça bouge pas mal sur le blog !&lt;/a&gt; (2019) - De 5s à 1s en virant Wordpress pour Hugo&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2025/01/15/ca-bouge-encore-sur-le-blog/" &gt;Ça bouge encore sur le blog&lt;/a&gt; (2025) - Nettoyage massif, retour auto-hébergé&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2023/08/01/automatiser-hugo-sans-github-action/" &gt;Automatiser son site Hugo sans Github Action ou Vercel&lt;/a&gt; - Le setup nginx + webhook actuel&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/02/19/optimisation-webperf-avif-precompression/" &gt;Optimisation webperf : AVIF et pré-compression&lt;/a&gt; - Le round précédent (focus sur les images)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="ce-que-teste-http-observatory"&gt;Ce que teste HTTP Observatory
&lt;/h2&gt;&lt;p&gt;HTTP Observatory vérifie une série de headers HTTP que votre serveur devrait envoyer pour protéger vos visiteurs. Les principaux :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Strict-Transport-Security (HSTS)&lt;/strong&gt; : force le navigateur à toujours utiliser HTTPS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content-Security-Policy (CSP)&lt;/strong&gt; : contrôle quelles ressources le navigateur peut charger (scripts, styles, images, iframes&amp;hellip;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X-Content-Type-Options&lt;/strong&gt; : empêche le navigateur de &amp;ldquo;deviner&amp;rdquo; le type MIME des fichiers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Referrer-Policy&lt;/strong&gt; : contrôle les informations de provenance envoyées aux sites tiers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X-Frame-Options&lt;/strong&gt; / &lt;strong&gt;frame-ancestors&lt;/strong&gt; : empêche l&amp;rsquo;inclusion de votre site dans une iframe (clickjacking)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sur un blog statique, on pourrait se dire que ce n&amp;rsquo;est pas critique. Pas de base de données, pas de sessions utilisateur, pas de formulaires sensibles. Mais ces headers protègent quand même contre des attaques réelles :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Un &lt;strong&gt;script injecté&lt;/strong&gt; (XSS) pourrait rediriger vos lecteurs vers un site malveillant&lt;/li&gt;
&lt;li&gt;Sans HSTS, un attaquant sur un Wi-Fi public pourrait intercepter la connexion initiale en HTTP&lt;/li&gt;
&lt;li&gt;Sans &lt;code&gt;frame-ancestors&lt;/code&gt;, quelqu&amp;rsquo;un pourrait inclure le blog dans une iframe piégée&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bref, même pour un blog statique, ça peut valoir le coup de faire l&amp;rsquo;effort, a minima pour s&amp;rsquo;habituer aux bonnes pratiques.&lt;/p&gt;
&lt;h2 id="étape-1--les-headers-faciles"&gt;Étape 1 : les headers &amp;ldquo;faciles&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Le piège nginx : &lt;code&gt;add_header&lt;/code&gt; et l&amp;rsquo;héritage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;La première chose, c&amp;rsquo;est que je suis tombé dans un piège classique avec mon nginx. Je l&amp;rsquo;ignorais, mais les directives &lt;code&gt;add_header&lt;/code&gt; dans un bloc &lt;code&gt;location&lt;/code&gt; &lt;strong&gt;écrasent&lt;/strong&gt; (et ne complètent pas) celles du bloc &lt;code&gt;server&lt;/code&gt; parent.&lt;/p&gt;
&lt;p&gt;Ça veut dire que si vous avez un &lt;code&gt;add_header Cache-Control&lt;/code&gt; dans une &lt;code&gt;location&lt;/code&gt; (c&amp;rsquo;était mon cas), tous vos headers de sécurité définis au niveau &lt;code&gt;server&lt;/code&gt; &lt;strong&gt;disparaissent&lt;/strong&gt; pour cette location. J&amp;rsquo;ai pété un plomb pendant quelques minutes à reload / restart en boucle sans comprendre.&lt;/p&gt;
&lt;p&gt;La solution : créer un fichier snippet et l&amp;rsquo;inclure partout.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &amp;gt; /etc/nginx/snippets/security-headers.conf &lt;span class="s"&gt;&amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;add_header Strict-Transport-Security &amp;#34;max-age=63072000; includeSubDomains; preload&amp;#34; always;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;add_header X-Content-Type-Options &amp;#34;nosniff&amp;#34; always;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;add_header Referrer-Policy &amp;#34;strict-origin-when-cross-origin&amp;#34; always;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;add_header Content-Security-Policy &amp;#34;...&amp;#34; always;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Puis dans la config nginx :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Headers de sécurité au niveau server
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/snippets/security-headers.conf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Cache des assets statiques
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;\.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg|webp|avif)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;1y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;public,&lt;/span&gt; &lt;span class="s"&gt;immutable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# OBLIGATOIRE : ré-inclure les headers ici !
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/snippets/security-headers.conf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;\.html&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;public,&lt;/span&gt; &lt;span class="s"&gt;must-revalidate&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/snippets/security-headers.conf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;HSTS, X-Content-Type-Options, Referrer-Policy&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ces trois-là sont les plus simples à ajouter et &amp;ldquo;ne cassent rien&amp;rdquo; :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HSTS&lt;/strong&gt; avec &lt;code&gt;max-age=63072000&lt;/code&gt; (2 ans) et &lt;code&gt;preload&lt;/code&gt; permet à terme (pas encore fait) de s&amp;rsquo;inscrire sur la &lt;a class="link" href="https://hstspreload.org/" target="_blank" rel="noopener"
&gt;HSTS preload list&lt;/a&gt;. Le navigateur refusera même la toute première connexion HTTP initiale.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nosniff&lt;/strong&gt; empêche les navigateurs d&amp;rsquo;interpréter un fichier texte comme du JavaScript.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;strict-origin-when-cross-origin&lt;/strong&gt; envoie l&amp;rsquo;URL complète en referrer pour les requêtes same-origin, mais seulement l&amp;rsquo;origine (domaine) pour les requêtes cross-origin.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="étape-2--content-security-policy-la-vraie-difficulté"&gt;Étape 2 : Content-Security-Policy (la vraie difficulté)
&lt;/h2&gt;&lt;p&gt;La CSP est un header puissant et a priori complexe. C&amp;rsquo;est lui qui rapporte le plus de points sur Observatory, mais c&amp;rsquo;est aussi lui qui peut tout casser si on va trop vite.&lt;/p&gt;
&lt;p&gt;Le principe : vous déclarez au navigateur &lt;strong&gt;exactement&lt;/strong&gt; quelles ressources votre site a le droit de charger (scripts, styles, images, iframes, fonts&amp;hellip;) et depuis quelles origines. Tout le reste est bloqué. C&amp;rsquo;est la meilleure défense contre le XSS, parce que même si un attaquant arrive à injecter un &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; dans votre HTML, le navigateur refusera de l&amp;rsquo;exécuter s&amp;rsquo;il n&amp;rsquo;est pas dans la liste autorisée.&lt;/p&gt;
&lt;p&gt;Le revers de la médaille : si vous oubliez une ressource légitime dans votre CSP, elle sera bloquée et votre site casse silencieusement : un script qui ne charge plus, une font qui disparaît, une iframe vide. Il faut donc faire un inventaire exhaustif &lt;em&gt;avant&lt;/em&gt; de verrouiller.&lt;/p&gt;
&lt;p&gt;Je me suis fait épauler par un LLM (Opus 4.6) pour les modifs à réaliser et on a fait plusieurs passes jusqu&amp;rsquo;à ce que tout fonctionne.&lt;/p&gt;
&lt;h3 id="inventaire-des-ressources-externes"&gt;Inventaire des ressources externes
&lt;/h3&gt;&lt;p&gt;Avant d&amp;rsquo;écrire la CSP, il faut faire l&amp;rsquo;inventaire de &lt;strong&gt;tout&lt;/strong&gt; ce que le site charge. Voilà une petite synthèse de mon pote &lt;em&gt;Claudio&lt;/em&gt; :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Ressource&lt;/th&gt;
&lt;th&gt;Origine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Script&lt;/td&gt;
&lt;td&gt;GoatCounter analytics&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://gc.zgo.at/count.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Script&lt;/td&gt;
&lt;td&gt;Umami analytics&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://cloud.umami.is/script.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect&lt;/td&gt;
&lt;td&gt;GoatCounter API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://blogzwindler.goatcounter.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect&lt;/td&gt;
&lt;td&gt;Umami API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://api-gateway.umami.dev&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Script&lt;/td&gt;
&lt;td&gt;De mon thème &amp;lsquo;stack&amp;rsquo; - vibrant.js (couleurs d&amp;rsquo;images)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://cdn.jsdelivr.net&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Script&lt;/td&gt;
&lt;td&gt;De mon thème &amp;lsquo;stack&amp;rsquo; - PhotoSwipe (lightbox images)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://cdn.jsdelivr.net&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Style&lt;/td&gt;
&lt;td&gt;De mon thème &amp;lsquo;stack&amp;rsquo; - PhotoSwipe CSS (lightbox)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://cdn.jsdelivr.net&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Style&lt;/td&gt;
&lt;td&gt;De mon thème &amp;lsquo;stack&amp;rsquo; - CSS du thème&lt;/td&gt;
&lt;td&gt;local (&lt;code&gt;'self'&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Font&lt;/td&gt;
&lt;td&gt;Luciole&lt;/td&gt;
&lt;td&gt;locale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frame&lt;/td&gt;
&lt;td&gt;Widget Deezer (inclu dans un seul article 😖)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://widget.deezer.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image&lt;/td&gt;
&lt;td&gt;data: URIs (SVGs inline)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form&lt;/td&gt;
&lt;td&gt;Mailchimp (abonnement newsletter)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://zwindler.us15.list-manage.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="le-problème-des-scripts-danalytics-externes"&gt;Le problème des scripts d&amp;rsquo;analytics externes
&lt;/h3&gt;&lt;p&gt;Avec une CSP comme &lt;code&gt;script-src 'self' https://gc.zgo.at https://cloud.umami.is&lt;/code&gt;, je devais soit :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Faire confiance à ces CDN&lt;/strong&gt; -&amp;gt; si le CDN est compromis, le script malveillant s&amp;rsquo;exécute sur mon blog. Nope.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ajouter des hashes SRI&lt;/strong&gt; (Subresource Integrity). Problème : ces scripts peuvent changer côté serveur sans prévenir, ce qui casserait le hash.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;J&amp;rsquo;ai choisi une troisième voie : &lt;strong&gt;télécharger les scripts à chaque build&lt;/strong&gt; et les servir localement.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Dans mon petit script blog_refresh.sh, avant le hugo --minify&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p static/js
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sf --max-time &lt;span class="m"&gt;10&lt;/span&gt; https://gc.zgo.at/count.js -o static/js/count.js &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;WARN: failed to download GoatCounter script&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sf --max-time &lt;span class="m"&gt;10&lt;/span&gt; https://cloud.umami.is/script.js -o static/js/umami.js &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;WARN: failed to download Umami script&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Et dans le template Hugo :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;data-goatcounter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;https://blogzwindler.goatcounter.com/count&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;async&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/js/count.js&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/js/umami.js&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;data-website-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;3d8f0ea9-...&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Est-ce que c&amp;rsquo;est &amp;ldquo;tricher&amp;rdquo; ?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Une fois que j&amp;rsquo;avais fait la modif, je me suis demandé : est-ce qu&amp;rsquo;héberger les scripts localement à chaque build ne contourne pas l&amp;rsquo;esprit des vérifications d&amp;rsquo;Observatory ? Sur ce point précis j&amp;rsquo;ai un petit doute, n&amp;rsquo;étant pas expert sur ces points là. J&amp;rsquo;ai demandé à un LLM qui m&amp;rsquo;a dit que &lt;em&gt;non&lt;/em&gt;, mais je veux bien un avis tiers.&lt;/p&gt;
&lt;p&gt;Dans tous les cas, ça semble être l&amp;rsquo;approche recommandée par &lt;a class="link" href="https://www.goatcounter.com/help/csp" target="_blank" rel="noopener"
&gt;la documentation GoatCounter&lt;/a&gt; pour les sites avec une CSP stricte.&lt;/p&gt;
&lt;h3 id="supprimer-unsafe-inline-de-script-src"&gt;Supprimer &lt;code&gt;unsafe-inline&lt;/code&gt; de &lt;code&gt;script-src&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;Observatory pénalise fortement &lt;code&gt;'unsafe-inline'&lt;/code&gt; dans &lt;code&gt;script-src&lt;/code&gt;. Et c&amp;rsquo;est normal, car c&amp;rsquo;est la porte ouverte au XSS : n&amp;rsquo;importe quel script injecté dans le HTML s&amp;rsquo;exécute.&lt;/p&gt;
&lt;p&gt;Le problème : mon thème Hugo (&lt;a class="link" href="https://github.com/CaiJimmy/hugo-theme-stack" target="_blank" rel="noopener"
&gt;hugo-theme-stack&lt;/a&gt;) générait &lt;strong&gt;4 scripts inline&lt;/strong&gt; :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Color scheme init&lt;/strong&gt; — lit le localStorage pour appliquer le thème clair/sombre avant le premier rendu&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Color scheme detection&lt;/strong&gt; — détecte la préférence système (&lt;code&gt;prefers-color-scheme: dark&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Language switcher&lt;/strong&gt; — le sélecteur de langue &lt;code&gt;&amp;lt;select onchange=&amp;quot;window.location.href=this.selectedOptions[0].value&amp;quot;&amp;gt;&lt;/code&gt; dans la sidebar, qui est un attribut d&amp;rsquo;événement inline (&lt;code&gt;script-src-attr&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Font loader&lt;/strong&gt; — charge la CSS de &lt;a class="link" href="https://luciole-vision.com/" target="_blank" rel="noopener"
&gt;la font Luciole&lt;/a&gt; dynamiquement&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Luciole: A typeface for visual impairment&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Pour chacun, j&amp;rsquo;ai extrait le code dans un fichier &lt;code&gt;.js&lt;/code&gt; externe dans &lt;code&gt;static/js/&lt;/code&gt; et remplacé le script inline par une balise &lt;code&gt;&amp;lt;script src=&amp;quot;...&amp;quot;&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Par exemple, pour le color scheme, le thème avait dans &lt;code&gt;layouts/partials/head/colorScheme.html&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;colorSchemeKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;StackColorScheme&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;colorSchemeKey&lt;/span&gt;&lt;span class="p"&gt;)){&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;colorSchemeKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;auto&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Que j&amp;rsquo;ai remplacé par un &lt;a class="link" href="https://gohugo.io/templates/lookup-order/" target="_blank" rel="noopener"
&gt;override de template Hugo&lt;/a&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- /* Override: external JS file instead of inline (CSP compliance) */ -}}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/js/color-scheme.js&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Avec le contenu correspondant dans &lt;code&gt;static/js/color-scheme.js&lt;/code&gt;. Hugo permet de surcharger n&amp;rsquo;importe quel template du thème en plaçant un fichier au même chemin dans le dossier &lt;code&gt;layouts/&lt;/code&gt; du projet.&lt;/p&gt;
&lt;p&gt;Pour le &lt;strong&gt;language switcher&lt;/strong&gt;, même approche : j&amp;rsquo;ai surchargé &lt;code&gt;layouts/partials/sidebar/left.html&lt;/code&gt; pour retirer l&amp;rsquo;attribut &lt;code&gt;onchange&lt;/code&gt; du &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; et ajouté un &lt;code&gt;id=&amp;quot;language-select&amp;quot;&lt;/code&gt;, puis créé un fichier &lt;code&gt;static/js/language-switcher.js&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;select&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;language-select&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;change&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selectedOptions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Note importante :&lt;/strong&gt; il m&amp;rsquo;a fallu plusieurs allers-retours avec la console du navigateur (F12 -&amp;gt; Console) avant de trouver la totalité des violations CSP. Chaque correction en révélait de nouvelles, il fallait bien regarder tous les différents types de pages sur le blog (pas juste la page d&amp;rsquo;accueil et les posts), et certaines erreurs affichées dans la console venaient en fait d&amp;rsquo;&lt;strong&gt;extensions navigateur&lt;/strong&gt; (typiquement les gestionnaires de mots de passe comme Bitwarden, dont le &lt;code&gt;bootstrap-autofill-overlay.js&lt;/code&gt; génère des violations &lt;code&gt;style-src-elem&lt;/code&gt;) et non du site lui-même. Il faut donc bien distinguer les erreurs du site de celles injectées par les extensions.&lt;/p&gt;
&lt;h3 id="supprimer-unsafe-inline-de-style-src-la-suite"&gt;Supprimer &lt;code&gt;unsafe-inline&lt;/code&gt; de &lt;code&gt;style-src&lt;/code&gt;, la suite
&lt;/h3&gt;&lt;p&gt;Même logique pour les styles. J&amp;rsquo;avais quatre sources d&amp;rsquo;inline CSS :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Un bloc &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;&lt;/strong&gt; dans &lt;code&gt;custom.html&lt;/code&gt; — les CSS custom properties pour ma font Luciole&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Des attributs &lt;code&gt;style=&amp;quot;&amp;quot;&lt;/code&gt;&lt;/strong&gt; sur les badges de catégories — &lt;code&gt;background-color: #2a9d8f; color: #fff;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Un attribut &lt;code&gt;style=&amp;quot;enable-background:...&amp;quot;&lt;/code&gt;&lt;/strong&gt; sur un SVG (export Adobe Illustrator)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Des attributs &lt;code&gt;style=&amp;quot;&amp;quot;&lt;/code&gt;&lt;/strong&gt; dans le formulaire Mailchimp du footer — &lt;code&gt;display:none&lt;/code&gt; et un positionnement off-screen pour le honeypot anti-spam&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Le &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; bloc&lt;/strong&gt; : déplacé dans &lt;code&gt;assets/scss/custom.scss&lt;/code&gt;, le fichier de personnalisation SCSS prévu par le thème.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-scss" data-lang="scss"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt;/* Font family CSS custom properties */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;--article-font-family&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Luciole&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;--base-font-family&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Luciole&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Les badges de catégories&lt;/strong&gt; : toutes mes catégories utilisent les mêmes couleurs (#2a9d8f / #fff), définies dans le front matter de chaque &lt;code&gt;_index.md&lt;/code&gt;. Le thème les injectait en &lt;code&gt;style=&amp;quot;&amp;quot;&lt;/code&gt; via le template &lt;code&gt;article/components/details.html&lt;/code&gt;. J&amp;rsquo;ai surchargé ce template pour retirer le &lt;code&gt;style&lt;/code&gt; inline et ajouté une règle CSS dans &lt;code&gt;custom.scss&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-scss" data-lang="scss"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nc"&gt;.article-category&lt;/span&gt; &lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;background-color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#2a9d8f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Le SVG&lt;/strong&gt; : j&amp;rsquo;avais un SVG que j&amp;rsquo;avais ajouté à la main (un petit Robot mignon pour mon &amp;ldquo;&lt;a class="link" href="https://blog.zwindler.fr/ai-manifesto/" target="_blank" rel="noopener"
&gt;AI Manifesto&lt;/a&gt;&amp;rdquo;) avec &lt;code&gt;enable-background&lt;/code&gt;, qui est un attribut SVG 1.1 déprécié. Je l&amp;rsquo;ai simplement supprimé du fichier SVG source, sans impact visuel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Le formulaire Mailchimp&lt;/strong&gt; : le code HTML Mailchimp utilise &lt;code&gt;style=&amp;quot;display:none&amp;quot;&lt;/code&gt; pour masquer les divs de feedback et &lt;code&gt;style=&amp;quot;position: absolute; left: -5000px;&amp;quot;&lt;/code&gt; pour le champ honeypot (pour les robots qui s&amp;rsquo;inscrivent à ma newsletter 🤔). J&amp;rsquo;ai remplacé tout ça par des classes CSS dans &lt;code&gt;custom.scss&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-scss" data-lang="scss"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;#mce-error-response&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;#mce-success-response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="ni"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nc"&gt;.mce-honeypot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="ni"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="la-csp-finale"&gt;La CSP finale
&lt;/h3&gt;&lt;p&gt;Après tous ces changements, plus aucun script ni style inline sur le site. La CSP peut donc être stricte :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;default-src &amp;#39;none&amp;#39;;
script-src &amp;#39;self&amp;#39; https://cdn.jsdelivr.net;
style-src &amp;#39;self&amp;#39; https://cdn.jsdelivr.net;
img-src &amp;#39;self&amp;#39; data:;
connect-src &amp;#39;self&amp;#39; https://blogzwindler.goatcounter.com https://api-gateway.umami.dev https://cdn.jsdelivr.net;
font-src &amp;#39;self&amp;#39;;
frame-src https://widget.deezer.com;
frame-ancestors &amp;#39;none&amp;#39;;
base-uri &amp;#39;self&amp;#39;;
form-action &amp;#39;self&amp;#39; https://zwindler.us15.list-manage.com;
manifest-src &amp;#39;self&amp;#39;;
media-src &amp;#39;self&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Points notables :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;default-src 'none'&lt;/code&gt;&lt;/strong&gt; : par défaut, rien n&amp;rsquo;est autorisé. Chaque type de ressource doit être explicitement listé. C&amp;rsquo;est la politique la plus restrictive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pas de &lt;code&gt;unsafe-inline&lt;/code&gt;&lt;/strong&gt; nulle part : ni dans &lt;code&gt;script-src&lt;/code&gt;, ni dans &lt;code&gt;style-src&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;frame-ancestors 'none'&lt;/code&gt;&lt;/strong&gt; remplace &lt;code&gt;X-Frame-Options: DENY&lt;/code&gt; (plus moderne)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cdn.jsdelivr.net&lt;/code&gt;&lt;/strong&gt; dans &lt;code&gt;script-src&lt;/code&gt;, &lt;code&gt;style-src&lt;/code&gt; ET &lt;code&gt;connect-src&lt;/code&gt; : le thème charge &lt;code&gt;vibrant.js&lt;/code&gt; (JS avec SRI), les CSS de PhotoSwipe (lightbox d&amp;rsquo;images) et les source maps associées depuis ce CDN&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;form-action&lt;/code&gt;&lt;/strong&gt; autorise Mailchimp pour le formulaire d&amp;rsquo;abonnement dans le footer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Au final, de nombreux morceaux du thème de mon blog ont du être réécrit / overridés. Je suis un peu gêné d&amp;rsquo;avoir du en arriver là, mais c&amp;rsquo;était le prix à payer pour une note maximale 😂.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/rework-theme.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="nettoyage-bonus--mailchimp"&gt;Nettoyage bonus : Mailchimp
&lt;/h2&gt;&lt;p&gt;En faisant l&amp;rsquo;inventaire des ressources externes, j&amp;rsquo;ai découvert un vieux widget Mailchimp dans la sidebar qui chargeait du CSS et du JavaScript depuis &lt;code&gt;chimpstatic.com&lt;/code&gt;. Je ne l&amp;rsquo;utilisais plus : supprimé. Le formulaire d&amp;rsquo;abonnement dans le footer des articles a été gardé, mais nettoyé de ses &lt;code&gt;style=&amp;quot;&amp;quot;&lt;/code&gt; inline (voir plus haut).&lt;/p&gt;
&lt;h2 id="résultat-final"&gt;Résultat final
&lt;/h2&gt;&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/observatory-a-plus.avif"
loading="lazy"
alt="A&amp;#43; sur HTTP Observatory"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;De F (0/100) à A+ (125/100).&lt;/strong&gt; Tous les tests sont verts.&lt;/p&gt;
&lt;p&gt;Un grand merci à Antoine pour sa patience avec les débutants naïfs comme moi 😂 et ses super talks !&lt;/p&gt;</description></item><item><title>Optimisation webperf : AVIF et pré-compression pour le blog</title><link>https://blog.zwindler.fr/2026/02/19/optimisation-webperf-avif-precompression/</link><pubDate>Thu, 19 Feb 2026 17:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/2026/02/19/optimisation-webperf-avif-precompression/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/02/webperf0.webp" alt="Featured image of post Optimisation webperf : AVIF et pré-compression pour le blog" /&gt;&lt;p&gt;Ce blog a presque 16 ans d&amp;rsquo;existence. Sur cette période, j&amp;rsquo;ai accumulé plus de 530 articles avec plus de 2700 images. Il y a quelques années, j&amp;rsquo;avais commencé à taper des limites (notamment quand j&amp;rsquo;ai essayer Gitlab pages chez Froggit) en atteignant les 500 Mo de medias.&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;avais fait une première passe d&amp;rsquo;optimisation, à grand coup de resize, &lt;code&gt;jpegoptim&lt;/code&gt; et &lt;code&gt;optipng&lt;/code&gt; et j&amp;rsquo;étais redescendu sous les 300 Mo. C&amp;rsquo;était pas mal, mais pas satisfaisant.&lt;/p&gt;
&lt;p&gt;Puis j&amp;rsquo;ai vu &lt;a class="link" href="https://blog.zwindler.fr/2026/02/13/recap-touraine-tech-2026-jour2/#au-secours--mes-images-pourrissent-mes-perfs" &gt;le talk d&amp;rsquo;Antoine Caron (slashgear) et Mathieu Mure à Touraine Tech 2026&lt;/a&gt; et j&amp;rsquo;ai enfin pris le temps de lâcher les &amp;ldquo;formats morts&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="un-peu-de-contexte"&gt;Un peu de contexte
&lt;/h2&gt;&lt;p&gt;Ça fait un moment que je bricole l&amp;rsquo;infra et les perfs de ce blog. Si ça vous intéresse, les épisodes précédents sont ici :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2019/12/24/ca-bouge-pas-mal-sur-le-blog/" &gt;Ça bouge pas mal sur le blog !&lt;/a&gt; (2019) - De 5s à 1s en virant Wordpress pour Hugo&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2025/01/15/ca-bouge-encore-sur-le-blog/" &gt;Ça bouge encore sur le blog&lt;/a&gt; (2025) - Nettoyage massif, retour auto-hébergé&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2023/08/01/automatiser-hugo-sans-github-action/" &gt;Automatiser son site Hugo sans Github Action ou Vercel&lt;/a&gt; - Le setup nginx + webhook actuel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quand j&amp;rsquo;ai commencé ce round d&amp;rsquo;optimisation, la note PageSpeed de la page d&amp;rsquo;accueil tournait autour de 70-85 en mobile selon les articles. Pas dramatique, mais on peut faire mieux !&lt;/p&gt;
&lt;h2 id="le-talk-qui-a-tout-déclenché"&gt;Le talk qui a tout déclenché
&lt;/h2&gt;&lt;p&gt;À &lt;a class="link" href="https://blog.zwindler.fr/2026/02/13/recap-touraine-tech-2026-jour2/" &gt;Touraine Tech 2026&lt;/a&gt;, Antoine Caron et Mathieu Mure ont fait un talk très clair sur l&amp;rsquo;optimisation des images web, qui pourrissent les perfs des sites web aujourd&amp;rsquo;hui.&lt;/p&gt;
&lt;p&gt;Le message principal : les formats comme JPEG et PNG sont des &amp;ldquo;formats morts&amp;rdquo; (ou en tout cas vieillissants). À compression équivalente, les formats modernes comme AVIF prennent beaucoup moins de place, mais surtout, ils affichent des artefacts visuels bien moindres dans les hauts niveaux de compression.&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;avais failli faire une migration vers le WebP il y a quelques années, puis j&amp;rsquo;avais laissé tomber, par flemme et après quelques soucis techniques dont je ne me souviens plus trop.&lt;/p&gt;
&lt;p&gt;Et finalement, c&amp;rsquo;est presque tant mieux, parce qu&amp;rsquo;avec AVIF, on peut compresser &lt;strong&gt;encore plus fort&lt;/strong&gt;, sans que ça se voit. C&amp;rsquo;est exactement ce qu&amp;rsquo;il me fallait.&lt;/p&gt;
&lt;p&gt;Side note : on m&amp;rsquo;a demandé une comparaison WebP vs AVIF, et Joseph a trouvé ça. C&amp;rsquo;est plutôt intéressant :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://elementor.com/blog/fr/avif-vs-webp-quel-format-dimage-regne-en-maitre-en-2024/" target="_blank" rel="noopener"
&gt;elementor blog - AVIF vs WebP : Quel format d’image règne en maître en 2024 ?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conversion-massive-en-avif"&gt;Conversion massive en AVIF
&lt;/h2&gt;&lt;p&gt;J&amp;rsquo;ai donc écrit un script qui :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Redimensionne&lt;/strong&gt; les images trop grandes (&amp;gt; 1500px) pour les ramener à ~1 mégapixel&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Convertit en AVIF&lt;/strong&gt; avec &lt;code&gt;avifenc&lt;/code&gt; (qualité 50, speed 6)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Met à jour les références&lt;/strong&gt; dans tous les articles markdown&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Et je l&amp;rsquo;ai lancé année par année, de 2026 jusqu&amp;rsquo;à 2010.&lt;/p&gt;
&lt;h3 id="les-résultats"&gt;Les résultats
&lt;/h3&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Année&lt;/th&gt;
&lt;th&gt;Fichiers&lt;/th&gt;
&lt;th&gt;Originaux&lt;/th&gt;
&lt;th&gt;AVIF&lt;/th&gt;
&lt;th&gt;Réduction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2010&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;0.4 MiB&lt;/td&gt;
&lt;td&gt;0.1 MiB&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2011&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;1.8 MiB&lt;/td&gt;
&lt;td&gt;0.7 MiB&lt;/td&gt;
&lt;td&gt;63%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2012&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0.4 MiB&lt;/td&gt;
&lt;td&gt;0.1 MiB&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2013&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0.1 MiB&lt;/td&gt;
&lt;td&gt;0.1 MiB&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2014&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;5.3 MiB&lt;/td&gt;
&lt;td&gt;1.0 MiB&lt;/td&gt;
&lt;td&gt;81%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;214&lt;/td&gt;
&lt;td&gt;10.7 MiB&lt;/td&gt;
&lt;td&gt;3.7 MiB&lt;/td&gt;
&lt;td&gt;65%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2016&lt;/td&gt;
&lt;td&gt;231&lt;/td&gt;
&lt;td&gt;12.1 MiB&lt;/td&gt;
&lt;td&gt;4.1 MiB&lt;/td&gt;
&lt;td&gt;66%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2017&lt;/td&gt;
&lt;td&gt;428&lt;/td&gt;
&lt;td&gt;23.7 MiB&lt;/td&gt;
&lt;td&gt;9.1 MiB&lt;/td&gt;
&lt;td&gt;62%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;142&lt;/td&gt;
&lt;td&gt;7.5 MiB&lt;/td&gt;
&lt;td&gt;2.4 MiB&lt;/td&gt;
&lt;td&gt;68%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2019&lt;/td&gt;
&lt;td&gt;136&lt;/td&gt;
&lt;td&gt;12.1 MiB&lt;/td&gt;
&lt;td&gt;2.9 MiB&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;228&lt;/td&gt;
&lt;td&gt;23.6 MiB&lt;/td&gt;
&lt;td&gt;5.6 MiB&lt;/td&gt;
&lt;td&gt;77%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2021&lt;/td&gt;
&lt;td&gt;187&lt;/td&gt;
&lt;td&gt;31.6 MiB&lt;/td&gt;
&lt;td&gt;4.8 MiB&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;237&lt;/td&gt;
&lt;td&gt;29.2 MiB&lt;/td&gt;
&lt;td&gt;7.5 MiB&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;253&lt;/td&gt;
&lt;td&gt;43.2 MiB&lt;/td&gt;
&lt;td&gt;7.8 MiB&lt;/td&gt;
&lt;td&gt;82%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;259&lt;/td&gt;
&lt;td&gt;34.2 MiB&lt;/td&gt;
&lt;td&gt;8.0 MiB&lt;/td&gt;
&lt;td&gt;77%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;31.2 MiB&lt;/td&gt;
&lt;td&gt;6.8 MiB&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;6.8 MiB&lt;/td&gt;
&lt;td&gt;1.8 MiB&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2704&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~274 MiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~66 MiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~76%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;De PNG/JPEG qualité 90 à AVIF qualité 50 : entre 4 et 5 fois moins lourd.&lt;/p&gt;
&lt;p&gt;Le plus satisfaisant, c&amp;rsquo;est que je ne suis pas capable de détecter visuellement de perte de qualité. Les screenshots de terminal et mes photos passent très bien en AVIF 50.&lt;/p&gt;
&lt;h2 id="pré-compression-des-documents-html"&gt;Pré-compression des documents HTML
&lt;/h2&gt;&lt;p&gt;Après la conversion AVIF, &lt;a class="link" href="https://bsky.app/profile/slashgear.dev/post/3metqwv5aas2t" target="_blank" rel="noopener"
&gt;Antoine Caron (@slashgear.dev)&lt;/a&gt; m&amp;rsquo;a fait remarquer un truc :&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Alors c&amp;rsquo;est pas mal déjà ! Je vois aussi que tes documents HTML ne sont pas compressés. Si ton blog est purement static, hésite pas à précompresser à balle et dire à ton server de servir les versions précompressées.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Effectivement, j&amp;rsquo;avais déjà &lt;code&gt;gzip on;&lt;/code&gt; dans ma config nginx, mais c&amp;rsquo;est de la compression &lt;strong&gt;à la volée&lt;/strong&gt;. Nginx utilise par défaut un niveau de compression modéré (niveau 6 sur 9) pour ne pas consommer trop de CPU.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;blog.zwindler.fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="s"&gt;/usr/share/nginx/html/blog.zwindler.fr/public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;gzip&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or, mon blog est &lt;strong&gt;100% statique&lt;/strong&gt;. Les fichiers ne changent qu&amp;rsquo;au rebuild. Ça veut dire qu&amp;rsquo;on peut les compresser une seule fois, avec le niveau maximum, et demander à nginx de servir directement les fichiers pré-compressés. Zéro CPU à chaque requête, mais surtout un meilleur ratio au final, car on peut compresser plus fort.&lt;/p&gt;
&lt;h3 id="côté-build--blog_refreshsh"&gt;Côté build : &lt;code&gt;blog_refresh.sh&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;J&amp;rsquo;ai ajouté les commandes de pré-compression après le &lt;code&gt;hugo --minify&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Pre-compress static files (gzip + brotli) for nginx gzip_static/brotli_static&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Zopfli: compatible gzip mais ~3-8% plus petit que gzip -9&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; -v zopfli &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&amp;gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find public -type f &lt;span class="se"&gt;\(&lt;/span&gt; -name &lt;span class="s2"&gt;&amp;#34;*.html&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.css&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.js&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.xml&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.json&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.svg&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.txt&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -exec zopfli --i1023 &lt;span class="o"&gt;{}&lt;/span&gt; +
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find public -type f &lt;span class="se"&gt;\(&lt;/span&gt; -name &lt;span class="s2"&gt;&amp;#34;*.html&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.css&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.js&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.xml&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.json&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.svg&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.txt&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -exec gzip -k -f -9 &lt;span class="o"&gt;{}&lt;/span&gt; +
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Brotli pre-compression (better ratio than gzip, ~15-25% smaller)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; -v brotli &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&amp;gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find public -type f &lt;span class="se"&gt;\(&lt;/span&gt; -name &lt;span class="s2"&gt;&amp;#34;*.html&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.css&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.js&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.xml&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.json&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.svg&amp;#34;&lt;/span&gt; -o -name &lt;span class="s2"&gt;&amp;#34;*.txt&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -exec brotli -k -f -q &lt;span class="m"&gt;11&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; +
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pour la compression gzip, Zigazou m&amp;rsquo;a conseillé d&amp;rsquo;utiliser &lt;a class="link" href="https://github.com/google/zopfli" target="_blank" rel="noopener"
&gt;Zopfli&lt;/a&gt; (&lt;code&gt;apt install zopfli&lt;/code&gt;) plutôt que &lt;code&gt;gzip -9&lt;/code&gt;. Zopfli produit des fichiers 100% compatibles gzip mais avec un meilleur ratio (~3-8% en moins). C&amp;rsquo;est plus lent, mais sur un blog statique où on compresse une seule fois au build, on s&amp;rsquo;en fiche.&lt;/p&gt;
&lt;p&gt;En pratique, le gain de Zopfli est surtout un bonus : la majorité des navigateurs modernes supportent Brotli et recevront les &lt;code&gt;.br&lt;/code&gt;, qui sont de toute façon plus petits. Le &lt;code&gt;.gz&lt;/code&gt; ne sert que de fallback.&lt;/p&gt;
&lt;p&gt;Chaque fichier &lt;code&gt;index.html&lt;/code&gt; se retrouve ainsi avec un &lt;code&gt;index.html.gz&lt;/code&gt; et un &lt;code&gt;index.html.br&lt;/code&gt; à côté de lui.&lt;/p&gt;
&lt;h3 id="côté-nginx"&gt;Côté nginx
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Serve pre-compressed files generated at build time
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;gzip_static&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;brotli_static&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# nécessite libnginx-mod-http-brotli-static
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Fallback pour les contenus non pré-compressés
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;gzip&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;gzip_vary&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;gzip_min_length&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;gzip_types&lt;/span&gt; &lt;span class="s"&gt;text/plain&lt;/span&gt; &lt;span class="s"&gt;text/css&lt;/span&gt; &lt;span class="s"&gt;text/xml&lt;/span&gt; &lt;span class="s"&gt;text/javascript&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;application/javascript&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;application/xml&lt;/span&gt; &lt;span class="s"&gt;image/svg+xml&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pour Brotli, sur &lt;strong&gt;Ubuntu 24.04+&lt;/strong&gt;, les paquets sont dans les dépôts officiels :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="le-gain-mesuré"&gt;Le gain mesuré
&lt;/h3&gt;&lt;p&gt;Un petit &lt;code&gt;curl&lt;/code&gt; pour comparer la page d&amp;rsquo;accueil :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Avec Brotli (ce que reçoivent les navigateurs)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -so /dev/null -w &lt;span class="s2"&gt;&amp;#34;%{size_download}&amp;#34;&lt;/span&gt; -H &lt;span class="s2"&gt;&amp;#34;Accept-Encoding: br, gzip&amp;#34;&lt;/span&gt; https://blog.zwindler.fr/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# =&amp;gt; 6 206 bytes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Sans compression&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -so /dev/null -w &lt;span class="s2"&gt;&amp;#34;%{size_download}&amp;#34;&lt;/span&gt; -H &lt;span class="s2"&gt;&amp;#34;Accept-Encoding: identity&amp;#34;&lt;/span&gt; https://blog.zwindler.fr/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# =&amp;gt; 32 886 bytes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;-81% sur le HTML&lt;/strong&gt;, de 33 Ko à 6 Ko transférés. Et on le vérifie dans Chrome DevTools : le header &lt;code&gt;Content-Encoding: br&lt;/code&gt; confirme que Brotli est bien servi.&lt;/p&gt;
&lt;p&gt;Et c&amp;rsquo;est pareil pour les autres fichiers textes statiques (CSS, JS).&lt;/p&gt;
&lt;h2 id="bilan"&gt;Bilan
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Optimisation&lt;/th&gt;
&lt;th&gt;Avant&lt;/th&gt;
&lt;th&gt;Après&lt;/th&gt;
&lt;th&gt;Gain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Images (AVIF)&lt;/td&gt;
&lt;td&gt;274 MiB&lt;/td&gt;
&lt;td&gt;66 MiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-76%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML page d&amp;rsquo;accueil (Brotli)&lt;/td&gt;
&lt;td&gt;33 Ko transférés&lt;/td&gt;
&lt;td&gt;6 Ko transférés&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-81%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Le tout sans aucune dégradation visible de la qualité des images, et zéro impact CPU côté serveur pour la compression (puisqu&amp;rsquo;elle est faite au build).&lt;/p&gt;
&lt;p&gt;Et des webperfs qui ont bien progressé, même en mobile :&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/webperf.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="addendum"&gt;Addendum
&lt;/h2&gt;&lt;p&gt;Un peu en vrac&amp;hellip;&lt;/p&gt;
&lt;p&gt;J&amp;rsquo;ai du repasser sur du webp pour les images en frontmatter car les sites sociaux (bluesky / slack / linkedin) ne supportent pas AVIF :-/.&lt;/p&gt;
&lt;p&gt;On m&amp;rsquo;a conseillé &lt;a class="link" href="https://github.com/google/zopfli" target="_blank" rel="noopener"
&gt;zopfli&lt;/a&gt; en remplacement de gzip.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Pourquoi ne pas utiliser zopfli &amp;ndash;i1023 en lieu et place de gzip -9 ?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sauf que :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;la très grande majorité supportent le brotli donc le gain sera marginal&lt;/li&gt;
&lt;li&gt;Sur ma mini VM, le zopfli n&amp;rsquo;aboutissait jamais (bug ou trop intensif en CPU)&lt;/li&gt;
&lt;li&gt;le projet n&amp;rsquo;est plus maintenu et qu&amp;rsquo;il a été archivé en octobre dernier.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sinon, il y a &lt;a class="link" href="https://github.com/ebiggers/libdeflate" target="_blank" rel="noopener"
&gt;libdeflate-gzip&lt;/a&gt;, sur le papier aussi bon que zopfli (pas testé) et semble encore maintenu.&lt;/p&gt;
&lt;p&gt;On m&amp;rsquo;a aussi indiqué qu&amp;rsquo;il y a un paramètre nginx pour hériter les headers&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header_inherit" target="_blank" rel="noopener"
&gt;https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header_inherit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Je déconseille nginx en tant qu'ingressController en production</title><link>https://blog.zwindler.fr/2022/11/14/je-deconseille-nginx-ingresscontroller-en-production/</link><pubDate>Mon, 14 Nov 2022 06:30:00 +0200</pubDate><guid>https://blog.zwindler.fr/2022/11/14/je-deconseille-nginx-ingresscontroller-en-production/</guid><description>&lt;img src="https://blog.zwindler.fr/2017/06/kubernetes2.webp" alt="Featured image of post Je déconseille nginx en tant qu'ingressController en production" /&gt;&lt;h2 id="disclaimer"&gt;Disclaimer
&lt;/h2&gt;&lt;p&gt;Je n&amp;rsquo;utilise plus nginx comme &lt;strong&gt;IngressController&lt;/strong&gt; Kubernetes depuis 2020 (même si j&amp;rsquo;ai écris quelques articles dessus, notamment &lt;a class="link" href="https://blog.zwindler.fr/2018/03/06/exposer-des-applications-kubernetes-en-dehors-des-cloud-providers-nginx-ingress-controller/" &gt;Exposer des applications containerisées Kubernetes (nginx Ingress Controller)&lt;/a&gt;). Aussi, il se peut que les informations contenues ici soient périmées.&lt;/p&gt;
&lt;p&gt;Cependant, des issues récentes faisant état du même problème que le miens sont régulièrement ouvertes donc je pense que c&amp;rsquo;est toujours le cas.&lt;/p&gt;
&lt;p&gt;• &lt;a class="link" href="https://github.com/kubernetes/ingress-nginx/issues/2461" target="_blank" rel="noopener"
&gt;github.com/kubernetes/ingress-nginx/issues/2461&lt;/a&gt;
• &lt;a class="link" href="https://github.com/kubernetes/ingress-nginx/issues/7115" target="_blank" rel="noopener"
&gt;github.com/kubernetes/ingress-nginx/issues/7115&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="contexte"&gt;Contexte
&lt;/h2&gt;&lt;p&gt;Pour qu&amp;rsquo;on mette tout de suite d&amp;rsquo;accord sur ce dont on parle, je ne déconseille évidemment pas l&amp;rsquo;usage de nginx en production. nginx est un soft robuste qui a fait ses preuves pendant des années et que j&amp;rsquo;utilise encore dans certains contextes pros et perso.&lt;/p&gt;
&lt;p&gt;Cependant, quand j&amp;rsquo;ai commencé à travailler avec Kubernetes, j&amp;rsquo;ai (comme beaucoup) lu la documentation.&lt;/p&gt;
&lt;p&gt;Et à l&amp;rsquo;époque, pour ce qui est de l&amp;rsquo;utilisation des Ingress, la documentation officielle mettait en avant l&amp;rsquo;implémentation de l&amp;rsquo;IngressController nginx disponible sur le Github de Kubernetes &lt;a class="link" href="https://github.com/kubernetes/ingress-nginx" target="_blank" rel="noopener"
&gt;github.com/kubernetes/ingress-nginx&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Point intéressant, il existe un autre dépôt mis à disposition par la société nginx inc, que je n&amp;rsquo;ai jamais utilisé &lt;a class="link" href="https://github.com/nginxinc/kubernetes-ingress" target="_blank" rel="noopener"
&gt;github.com/nginxinc/kubernetes-ingress.&lt;/a&gt; qui n&amp;rsquo;est peut être pas affecté. Cependant quand on parle de &amp;ldquo;l&amp;rsquo;IngressController nginx&amp;rdquo; la plupart des gens parle du repo sur le Github de Kubernetes (source : moi, tkt)&lt;/p&gt;
&lt;h2 id="et-donc-quel-est-le-problème-"&gt;Et donc quel est le problème ?
&lt;/h2&gt;&lt;p&gt;Maintenant qu&amp;rsquo;on est bien d&amp;rsquo;accord que ce dont on parle, je peux vous raconter une petite histoire.&lt;/p&gt;
&lt;p&gt;A l&amp;rsquo;époque je travaillais pour une entreprise industrielle qui hébergeait &amp;ldquo;dans le cloud&amp;rdquo; un logiciel vendu en SaaS.&lt;/p&gt;
&lt;p&gt;Chaque client avait sa propre instance, hébergée sur un Kubernetes hébergé sur Azure.&lt;/p&gt;
&lt;p&gt;Au début nous avions peu de clients, tout allait bien. Mais au fur et à mesure que le nombre de clients grossissait, on commençait à avoir des retours de la part des équipes métiers et support comme quoi, de temps en temps, l&amp;rsquo;application perdait temporairement la connexion au serveur.&lt;/p&gt;
&lt;p&gt;En bon sysadmin et après avoir vérifié l&amp;rsquo;absence d&amp;rsquo;alertes de notre côté, on a d&amp;rsquo;abord blâmé l&amp;rsquo;utilisateur, le réseau et l&amp;rsquo;application elle même (dans cet ordre). &lt;strong&gt;#Trollface&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Blague à part, la réalité, c&amp;rsquo;est que nous ne savions pas trop par où prendre le problème car nous n&amp;rsquo;avions aucun log, aucun incident et le coupures étaient au début très rares et surtout aléatoires en apparence.&lt;/p&gt;
&lt;h2 id="jusquau-jour-où"&gt;Jusqu&amp;rsquo;au jour où&amp;hellip;
&lt;/h2&gt;&lt;p&gt;Jusqu&amp;rsquo;au jour où, pour des histoires de CVEs à patcher, nous avons redéployé un très grand nombre d&amp;rsquo;applications dans un créneau horaire assez court.&lt;/p&gt;
&lt;p&gt;A ce moment là, le nombre de plaintes a fortement augmenté et nous a permis de valider que le problème n&amp;rsquo;était pas aléatoire et était bien de notre côté.&lt;/p&gt;
&lt;p&gt;Nous avons deviné qu&amp;rsquo;il y avait une corrélation entre le déploiement d&amp;rsquo;applications dans Kubernetes et la coupure des connexions (websockets) côté client.&lt;/p&gt;
&lt;h2 id="on-est-pas-seul"&gt;On est pas seul
&lt;/h2&gt;&lt;p&gt;Une fois qu&amp;rsquo;on savait ça, on a trouvé le coupable assez vite. Une rapide recherche Google nous a permis de tomber sur plusieurs issues sur le repository de l&amp;rsquo;IngressController, décrivant les mêmes symptômes.&lt;/p&gt;
&lt;p&gt;En creusant un peu, on a également trouvé un article très complet qui détaille bien mieux que ce que j&amp;rsquo;aurais pu faire le problème.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://danielfm.me/post/painless-nginx-ingress/" target="_blank" rel="noopener"
&gt;DanielFM - pain(less) NGINX Ingress&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="cest-quoi-le-problème-"&gt;C&amp;rsquo;est quoi le problème ???
&lt;/h2&gt;&lt;p&gt;TL;DR tel que l&amp;rsquo;IngressController est implémenté par défaut, la configuration de nginx est rechargée à chaque fois qu&amp;rsquo;une modification a lieu sur les backends des Services Kubernetes.&lt;/p&gt;
&lt;p&gt;Pour ceux qui ne sont pas encore bien familier avec ces concepts, à chaque fois qu&amp;rsquo;un Pod (votre app containerisée) est créé ou détruit, nginx est rechargé.&lt;/p&gt;
&lt;p&gt;Théoriquement, ça se fait à chaud car nginx supporte les rechargements de configuration à chaud. Sauf que, tel que l&amp;rsquo;IngressController est implémenté, les Websockets ouvertes doivent être coupées&amp;hellip;&lt;/p&gt;
&lt;p&gt;Pour limiter la casse, à chaque rechargement de configuration, un processus nginx avec la nouvelle configuration est généré en parallèle de l&amp;rsquo;ancien qui est gardé au maximum 10 secondes pour donner une chance à toutes les connexions en cours de se fermer proprement.&lt;/p&gt;
&lt;h2 id="solutions-"&gt;Solutions ?
&lt;/h2&gt;&lt;p&gt;Dans son post, Daniel fait le tour des workarounds à votre disposition pour limiter la casse.&lt;/p&gt;
&lt;p&gt;La première chose qu&amp;rsquo;on peut faire et qui marche bien est d&amp;rsquo;ajouter l&amp;rsquo;option &lt;code&gt;--enable-dynamic-configuration&lt;/code&gt; qui fait que la configuration n&amp;rsquo;est plus rechargée à chaque changement de backends sur les services (événements hyper fréquent) mais uniquement lors des créations/suppression de services (le déploiement d&amp;rsquo;une nouvelle app).&lt;/p&gt;
&lt;p&gt;Ça limite drastiquement le nombre de reloads mais n&amp;rsquo;est évidemment pas une solution.&lt;/p&gt;
&lt;p&gt;Le deuxième levier mis en avant par Daniel est d&amp;rsquo;augmenter le timeout de 10s (&lt;code&gt;--worker-shutdown-timeout&lt;/code&gt;) et de mettre quelque chose de très grand. Cependant, il met en garde contre cette solution car on se retrouve vite avec un duplication des processus nginx avec X versions de la conf, ce qui peut faire saturer la RAM de votre ingressController ou votre node&amp;hellip;&lt;/p&gt;
&lt;p&gt;Dernière idée, pour limiter la disruption causée par un changement, nous avions envisagé de segmenter en ayant un ingressController par namespaces. Ainsi en cas de redéploiement, seul les applications du namespace en cours de déploiement seraient concernées par la coupure (des « workarounds » plutôt) :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Limiter les reloads (pénible)&lt;/li&gt;
&lt;li&gt;Augmenter sensiblement (24h ?) les timeout sur les connexions actives&lt;/li&gt;
&lt;li&gt;Segmenter les namespaces (1 Ingress Controller par namespace, on limite l&amp;rsquo;impact d&amp;rsquo;un redéploiement à un client)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="mais-alors-on-fait-quoi-"&gt;Mais alors on fait quoi ?
&lt;/h2&gt;&lt;p&gt;Comme il n&amp;rsquo;y a pas vraiment de solution à date, le plus simple reste de ne tout simplement pas utiliser l&amp;rsquo;IngressController nginx de Kubernetes 😉 et de lui préférer une implémentation gérant ce genre de cas à chaud.&lt;/p&gt;
&lt;p&gt;Même si je n&amp;rsquo;ai pas retravaillé avec depuis un moment, nous avions eu à l&amp;rsquo;époque de bons résultats avec &lt;a class="link" href="https://traefik.io/solutions/kubernetes-ingress/" target="_blank" rel="noopener"
&gt;Traefik&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Et si vous en connaissez d&amp;rsquo;autres qui gèrent bien ce genre de cas, n&amp;rsquo;hésitez pas à me l&amp;rsquo;indiquer :).&lt;/p&gt;</description></item><item><title>La rentrée 2020 du blog : pas mal de changements, encore !!</title><link>https://blog.zwindler.fr/2020/09/14/2020-sur-le-blog-pas-mal-de-changements-encore/</link><pubDate>Mon, 14 Sep 2020 06:05:00 +0000</pubDate><guid>https://blog.zwindler.fr/2020/09/14/2020-sur-le-blog-pas-mal-de-changements-encore/</guid><description>&lt;img src="https://blog.zwindler.fr/2017/11/blog.zwindler.fr_is_over_9000.webp" alt="Featured image of post La rentrée 2020 du blog : pas mal de changements, encore !!" /&gt;&lt;h2 id="2020-lannée-du-premier-confinement-trollface"&gt;2020, l’année du premier confinement (#trollface)
&lt;/h2&gt;&lt;p&gt;En décembre dernier, j’avais fais un petit recap’ de &amp;ldquo;mi-année&amp;rdquo; (d’habitude je le fais plutôt à l’anniversaire du blog) car beaucoup de choses &lt;a class="link" href="https://blog.zwindler.fr/2019/12/24/ca-bouge-pas-mal-sur-le-blog/" &gt;avaient été changées sur le blog&lt;/a&gt;. Notamment, j’avais fais un gros effort sur la vie privée, en retirant le plus de trackers possibles (Google Analytics, AMP, Mailchimp, Adwords) et un peu sur la perf.&lt;/p&gt;
&lt;p&gt;Il restait du boulot mais vous allez voir, je n’ai pas chaumé ;-).&lt;/p&gt;
&lt;h2 id="trackers-et-cookies"&gt;Trackers et cookies
&lt;/h2&gt;&lt;p&gt;Dans l’article précédent, j’indiquais que j’avais retiré les boutons de partages (Facebook, Twitter, etc), car ils contenaient du code leakant des données privées. Seboss666 m’a fait remarquer en commentaire qu’il suffisait de copier les icônes et le code que lui utilisait (eux mêmes basés sur un tuto de Korben d’il y a perpet’), ce que j’ai partiellement fais. Les boutons de partages sont donc revenus, respectueux de votre vie privée :)&lt;/p&gt;
&lt;p&gt;Un autre souci que j’avais était que j’utilise l’extension de WordPress Jetpack, qui aujourd’hui plein de features à WordPress dont certaines pas très très glop en terme de vie privée.&lt;/p&gt;
&lt;p&gt;Notamment, des trackers (nécessaires pour bénéficier des statistiques des visites). Comme je suis passé sur Matomo, les statistiques de WordPress font double emploi mais j’ai mis du temps à trouver comment &lt;em&gt;désactiver uniquement les statistiques&lt;/em&gt; et pas le reste (j’utilise Akismet pour les spam en commentaire et Markdown pour la rédaction).&lt;/p&gt;
&lt;p&gt;En réalité, les coquins (de chez WordPress) ont cachés l’option (qui était visible il y a 3 ans), mais elle est toujours disponible moyennant une petit bidouille, détaillée dans &lt;a class="link" href="https://wordpress.org/support/topic/disable-jetpack-stats-no-more-option/" target="_blank" rel="noopener"
&gt;ce topic sur le support de WordPress&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2020/07/wordpress_stats.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Maintenant, WordPress ne collecte plus vos données quand vous visitez le site. Et ça c’est cool :)
Et enfin, j’ai changé l&amp;rsquo;email permettant de me contacter a propos du blog. Précédemment, le compte mail pour me contacter était en gmail. Pour plus de cohérence, j’ai migré le compte sur un compte protonmail (&lt;a class="link" href="mailto:zwindl3r@protonmail.com" &gt;zwindl3r@protonmail.com&lt;/a&gt;), &lt;em&gt;a priori&lt;/em&gt; un peu plus respectueux sur la vie privée (enfin, c’est ce qu’ils disent)&amp;hellip;&lt;/p&gt;
&lt;h2 id="amélioration-diverses"&gt;Amélioration diverses
&lt;/h2&gt;&lt;p&gt;J’ai vu passer cet article de &lt;a class="link" href="https://y0no.fr/posts/decouverte-security-txt/" target="_blank" rel="noopener"
&gt;y0no a propos des security.txt&lt;/a&gt;. Pour faire simple, il s’agit d’un draft de RFC permettant aux hackers éthiques (ou simplement aux gens qui trouvent un trou de sécu béant dans votre site) de disposer des informations pour vous prévenir.&lt;/p&gt;
&lt;p&gt;L’idée est tellement bonne et coûte tellement peu cher à mettre en place (un fichier texte de quelques lignes) que je vous invite très fortement à le mettre en place vous aussi :) !&lt;/p&gt;
&lt;p&gt;J’ai refais une passe sur les mentions légales (&lt;a class="link" href="https://blog.zwindler.fr/mentions-l%c3%a9gales/" &gt;disponibles ici&lt;/a&gt;). Il s’agit d’un modèle généré via un site web (&lt;a class="link" href="https://www.subdelirium.com/generateur-de-mentions-legales/" target="_blank" rel="noopener"
&gt;subdelirium.com&lt;/a&gt;) qui n’étaient plus tout à fait à jour.&lt;/p&gt;
&lt;p&gt;Enfin, pour le lulz, j’ai voulu faire un test de PRA du blog. Car, oui, j’ai un plan de reprise d’activité pour mon blog perso, comme tous les admin systèmes tarés ;-p.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2020/07/cavacouper.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Et bien sûr, comme tout test, ça a misérablement échoué car ma procédure de PRA n’était pas à jour. Après 30 minutes d’indispo, j’ai retrouvé les bonnes commandes&amp;hellip; et j’ai rédigé un Post Mortem (dans un thread Twitter) !&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2020/07/postmortem.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="performances"&gt;Performances
&lt;/h2&gt;&lt;p&gt;L’an dernier, j’avais fais un GROS travail pour améliorer les performances du blog. Certaines pages mettaient extrêmement longtemps à s’afficher, ce qui n’avait pas vraiment de sens. Après un gros travail de nettoyage des plugins, mise à jour de la stack technique et ajout de cache, j’avais réussi à descendre sous la seconde.&lt;/p&gt;
&lt;p&gt;Histoire d’optimiser encore un peu plus, j’ai suivi quelques tutos dénichés ça et là :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.abyssproject.net/2020/05/mettre-en-place-les-images-au-format-webp-sur-son-site-avec-nginx/" target="_blank" rel="noopener"
&gt;Mise en place de la distribution des images au format wepb&lt;/a&gt; (merci Nicolas Simond)&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.abyssproject.net/2020/06/diverses-astuces-pour-loptimisation-dun-wordpress/" target="_blank" rel="noopener"
&gt;Diverses astuces pour l’optimisation d’un WordPress&lt;/a&gt; (encore lui !)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Voici également quelques outils qui m’ont permis de savoir vers quoi concentrer mes efforts pour améliorer les performances générales :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://gtmetrix.com/" target="_blank" rel="noopener"
&gt;GTmetrix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://developers.google.com/speed/pagespeed/insights/" target="_blank" rel="noopener"
&gt;Pagespeed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://tools.pingdom.com/" target="_blank" rel="noopener"
&gt;Tools Pingdom&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2020/07/pingdom-1.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Je me suis concentré sur ces trois outils pour vraiment trouver les causes des problèmes de performances, mais vous pouvez en trouver d’autres (pour d’autres sujets) sur &lt;a class="link" href="https://lord.re/posts/124-site-outils-amelioration-sites/" target="_blank" rel="noopener"
&gt;ce billet qui en liste plein d’autres&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Et enfin, plutôt que de passer par une extension pour gérer le caching du blog, j’ai correctement configuré mon frontal nginx pour le faire :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.nginx.com/blog/nginx-caching-guide/" target="_blank" rel="noopener"
&gt;A Guide to Caching with NGINX&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.nginx.com/nginx/admin-guide/content-cache/content-caching/" target="_blank" rel="noopener"
&gt;NGINX Content Caching&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="audimétrie"&gt;Audimétrie
&lt;/h2&gt;&lt;p&gt;Le blog a connu une TRÈS forte augmentation lors de la période de confinement en France, notamment car j’avais fait plusieurs articles sur la &lt;a class="link" href="https://blog.zwindler.fr/2020/03/17/ta-visio-open-source-comme-un-pro-avec-jitsi/" &gt;mise en place de Jitsi pour la visioconférence&lt;/a&gt;, avec un pic en avril à plus de 29000 vues, &lt;em&gt;sans compter&lt;/em&gt; les visiteurs qui ont activé l’entête DoNotTrack (entre 25 et 50% du trafic sur mon blog, soit plus de 50k).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2020/07/traffic_matomo.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;C’est assez colossal ; même si c’est un peu redescendu depuis (notamment cet été mais c’est tous les ans comme ça), autour de 20000-25000 vues par mois (~35000 si on ignore DNT), ça représente le double de trafic par rapport à l’an dernier, ce qui est complètement fou.&lt;/p&gt;
&lt;p&gt;Cet été, vous avez été très nombreux à réagir à deux articles (dont un en deux parties), moins techniques :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2020/07/29/au-secours-le-metier-dops-va-disparaitre/" &gt;Au secours, le métier d’Ops va disparaître !&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2020/08/17/mise-en-place-dune-astreinte-partie-1/" &gt;Mise en place d’une astreinte OPS - partie 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;J’essayerai de continuer à vous proposer, de temps en temps, ce genre de contenu, même si étonnamment ils réclament beaucoup plus d’efforts à rédiger.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Je suis très touché par tous les retours que j’ai eu ces derniers mois alors une fois de plus, je vous remercie chaleureusement.&lt;/p&gt;
&lt;p&gt;Sur ces mots, je vous souhaite une bonne rentrée et en attendant de se retrouver : &lt;strong&gt;Have fun !&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>Mes stats dans Hugo sans Google Analytics (Matomo)</title><link>https://blog.zwindler.fr/2019/06/18/mes-stats-de-visites-hugo-sans-google-analytics-avec-matomo/</link><pubDate>Tue, 18 Jun 2019 11:45:24 +0000</pubDate><guid>https://blog.zwindler.fr/2019/06/18/mes-stats-de-visites-hugo-sans-google-analytics-avec-matomo/</guid><description>&lt;img src="https://blog.zwindler.fr/2019/06/hugo_matomo.webp" alt="Featured image of post Mes stats dans Hugo sans Google Analytics (Matomo)" /&gt;&lt;h2 id="matomo-pour-faire-quoi-"&gt;Matomo, pour faire quoi ?
&lt;/h2&gt;&lt;p&gt;La semaine dernière j’ai écris un article pour expliquer que j’étais en train de &lt;a class="link" href="https://blog.zwindler.fr/2019/06/10/comment-migrer-de-wordpress-a-hugo/" &gt;migrer de WordPress vers Hugo, mais que ce n’était pas si facile&lt;/a&gt;. D’abord, parce que WordPress fait PLEIN de choses (et c’est à la fois son avantage et son inconvénient) et notamment vous fournir des stats sur vos visiteurs. Et, vous vous en doutez, avec un site statique, on n’a pas ça. C’est là que Matomo rentrera en jeu&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2019/06/wordpress_stats.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="écraser-une-mouche-avec-un-bulldozer"&gt;Écraser une mouche avec un bulldozer
&lt;/h2&gt;&lt;p&gt;Alors OK, les statistiques fournies par Jetpack, ce n’est pas forcément un truc fou non plus.&lt;/p&gt;
&lt;p&gt;En réalité, on pourrait se rapprocher de ce qu’on peut faire avec WordPress en terme de statistiques avec ce que j’avais fais lorsque &lt;a class="link" href="https://blog.zwindler.fr/2017/10/17/elasticstack-afficher-les-donnees-dans-kibana/" &gt;je testais ElasticStack et en récupérant les stats de nginx&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2017/10/elk16.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Mais clairement, pour en arriver au même niveau de détail, il va falloir passer pas mal de temps à nettoyer les données de références (les logs nginx) et à concevoir les tableaux de bord (dans Kibana).&lt;/p&gt;
&lt;p&gt;Au final, on pourra même aller encore plus loin, mais ça sera nécessairement très coûteux en temps de développement / configuration.&lt;/p&gt;
&lt;h2 id="méfie-toi-du-côté-obscur-plus-rapide-plus-facile-plus-séduisant"&gt;Méfie-toi du côté obscur. Plus rapide, plus facile, plus séduisant
&lt;/h2&gt;&lt;p&gt;Si c’est la simplicité que l’on cherche, alors clairement, le plus facile sera probablement d’ouvrir un compte Google Analytics et d’ajouter le petit bout de code fourni clé en main.&lt;/p&gt;
&lt;p&gt;De ce point de vue, ça sera clairement le plus simple et vous aurez à votre disposition un outil à la fois simple et puissant. Et qu’importe si Google &lt;del&gt;espionne&lt;/del&gt; profile (un peu plus) vos utilisateurs ?&lt;/p&gt;
&lt;p&gt;Je vais partir du principe que si vous auto-hébergez comme moi, vous n’avez pas envie de faciliter la tâche à Google. Et c’est donc pour cela que je vous propose une autre option : &lt;a class="link" href="https://matomo.org/" target="_blank" rel="noopener"
&gt;Matomo&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="donc-on-ne-sait-toujours-pas-ce-que-cest-que-matomo-en-fait"&gt;Donc on ne sait toujours pas ce que c’est que Matomo, en fait&amp;hellip;
&lt;/h2&gt;&lt;p&gt;Pour ceux qui l’ont connu, &lt;a class="link" href="https://matomo.org/" target="_blank" rel="noopener"
&gt;Matomo&lt;/a&gt; est le successeur de Piwik (ça vous parle peut être plus, le projet a été renommé en 2018) et permet d’obtenir un outil similaire à Google Analytics, mais open source ET que vous pouvez auto-héberger.
Exit donc, Google, pour un résultat similaire et le tout chez vous.
A noter, Matomo propose également une plateforme Analytics managée (payante), qui fera office de concurrent stricto sensu à Google Analytics et a priori plus vertueux envers la vie privée de vos utilisateurs (mais je n’ai pas vérifié).&lt;/p&gt;
&lt;h2 id="le-usecase"&gt;Le usecase
&lt;/h2&gt;&lt;p&gt;J’ai donc sur les bras une plateforme Hugo pour le blog, sans aucune stat. Dans mon cas, Hugo est hébergé sur une plateforme CentOS / nginx. Idéalement, j’aimerai bien pouvoir garder mon Matomo sur la même machine, pour faire simple.&lt;/p&gt;
&lt;p&gt;Et malheureusement pour moi, la plupart des gens qui ont fait des tutos sur le net ont eux installés ça sur Ubuntu (ça c’est respectable) et souvent sur Apache HTTPd (là par contre, emoji-qui-vomi).&lt;/p&gt;
&lt;h2 id="installation-des-prérequis"&gt;Installation des prérequis
&lt;/h2&gt;&lt;p&gt;C’est donc parti pour installer Matomo dans un premier temps (on verra ensuite pour Hugo). La stack technique de Matomo étant basée sur PHP/MySQL, on va devoir installer php-fpm pour le moteur PHP et MariaDB.&lt;/p&gt;
&lt;p&gt;Et comme sur CentOS, PHP et ses dépendances datent de Mathusalem, on va faire appel une fois de plus aux répos de rémi. Encore une fois, merci à lui&amp;hellip;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo yum install epel-release
sudo yum install http://rpms.remirepo.net/enterprise/remi-release-7.rpm
sudo yum install yum-utils
sudo yum-config-manager --enable remi-php72
sudo yum update
sudo yum install mariadb-server nginx php-fpm php-mysql php-mbstring php-dom php-xml php-gd freetype unzip wget
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Dans le cas (probable) où vous souhaitez aussi avoir une meilleure localisation de vos lecteurs/visiteurs, il faudra ajouter des modules complémentaire pour GeoIP&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo yum install gcc php-devel
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Maintenant que c’est fait, on peut démarrer php-fpm et l’activer au démarrage de la machine&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo systemctl start php-fpm
sudo systemctl enable php-fpm
sudo systemctl start mariadb
sudo systemctl enable mariadb
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As usual, quand on installe pour la première fois mariadb (ou mysql), on n’oublie surtout pas de passer le petit tool de nettoyage/sécurisation &amp;ldquo;minimal&amp;rdquo; :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mysql_secure_installation
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ok, on a un moteur de base de données utilisable maintenant. On va donc créer une base et un utilisateur dédiés à Matomo :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mysql -u root -p
mysql&amp;gt; CREATE DATABASE matomo;
mysql&amp;gt; CREATE USER &amp;#39;matomo&amp;#39;@&amp;#39;localhost&amp;#39; IDENTIFIED BY &amp;#39;myawesomepassword&amp;#39;;
mysql&amp;gt; GRANT ALL ON matomo.* TO &amp;#39;matomo&amp;#39;@&amp;#39;localhost&amp;#39;;
mysql&amp;gt; FLUSH PRIVILEGES;
exit
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="installer-matomo"&gt;Installer Matomo
&lt;/h2&gt;&lt;p&gt;On a fait le tour des prérequis. On peut maintenant installer et commencer à configurer notre Matomo. Le plus simple reste de télécharger le dernier build sur le site de matomo, et de déposer les sources dans notre &amp;ldquo;dossier root&amp;rdquo; nginx :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cd /tmp
wget https://builds.matomo.org/matomo-latest.zip
unzip matomo-latest.zip
sudo mv matomo /usr/share/nginx/html
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Dans les prérequis que vous auriez peut être loupés, Matomo conseille également d’ajouter un job de nettoyage dans la crontab. Ce n’est pas nécessaire pour démarrer, mais autant faire les choses bien dès le début. Pour faire simple, j’ai créé un fichier matomo-archive dans &lt;em&gt;/etc/cron.d&lt;/em&gt; (mais un crontab -e ferait tout aussi bien l’affaire) :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cat /etc/cron.d/matomo-archive
MAILTO=&amp;#34;awesome@email.provider&amp;#34;
5 * * * * nginx /usr/bin/php /usr/share/nginx/html/matomo/console core:archive --url=https://matomo.youexample.org/ &amp;gt; /var/log/nginx/matomo.archive.log
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ensuite, je vais partir du principe que vous allez exposer votre Matomo sur Internet, donc forcément en 443, avec une génération de certificat automatique via &lt;a class="link" href="https://blog.zwindler.fr/recherche/?keyword=let%27s&amp;#43;encrypt" &gt;Let’s Encrypt&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Là c’est clairement un peu plus touchy, donc si vous voulez plus de détails sur le pourquoi du comment de chaque bloc de configuration nginx, je vous renvoie au &lt;a class="link" href="https://github.com/matomo-org/matomo-nginx/blob/master/sites-available/matomo.conf" target="_blank" rel="noopener"
&gt;fichier d’exemple disponible sur le github de Matomo&lt;/a&gt;. La subtilité par rapport au fichier d’exemple est que comme nous sommes sur CentOS et pas Ubuntu, on n’utilise pas le socket unix pour accéder à php-fpm, mais le port 9000&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo vi /etc/nginx/sites-available/matomo.example.org.conf
server {
listen 443 ssl http2;
server_name matomo.example.org;
ssl_certificate /etc/letsencrypt/live/matomo.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matomo.example.org/privkey.pem;
index index.php;
root /usr/share/nginx/html/matomo;
access_log /var/log/nginx/matomo.access.log;
error_log /var/log/nginx/matomo.error.log;
index index.php;
location ~ ^/(index|matomo|piwik|js/index).php {
include fastcgi.conf;
fastcgi_param HTTP_PROXY &amp;#34;&amp;#34;;
fastcgi_pass 127.0.0.1:9000;
}
location = /plugins/HeatmapSessionRecording/configs.php {
include fastcgi.conf;
fastcgi_param HTTP_PROXY &amp;#34;&amp;#34;;
fastcgi_pass 127.0.0.1:9000;
}
location ~* ^.+\.php$ {
deny all;
return 403;
}
location / {
try_files $uri $uri/ =404;
}
location ~ /(config|tmp|core|lang) {
deny all;
return 403;
}
location ~ /\.ht {
deny all;
return 403;
}
location ~ \.(gif|ico|jpg|png|svg|js|css|htm|html|mp3|mp4|wav|ogg|avi|ttf|eot|woff|woff2|json)$ {
allow all;
expires 1h;
add_header Pragma public;
add_header Cache-Control &amp;#34;public&amp;#34;;
}
location ~ /(libs|vendor|plugins|misc/user) {
deny all;
return 403;
}
location ~/(.*\.md|LEGALNOTICE|LICENSE) {
default_type text/plain;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Maintenant que notre matomo est prêt à être accéder depuis l’extérieur, n’oubliez pas de :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Créer le record DNS pour matomo.example.org&lt;/li&gt;
&lt;li&gt;Générer le certificat Let’s Encrypt&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Activez le site en créant un lien symbolique entre sites-enabled et sites-available, vérifiez la conf et rechargez nginx&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo ln -s /etc/nginx/sites-available/matomo.example.org.conf /etc/nginx/sites-enabled/matomo.example.org.conf
nginx -t
sudo systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A partir de maintenant, le site est accessible, à l’URL &lt;a class="link" href="https://matomo.example.org/index.php" target="_blank" rel="noopener"
&gt;https://matomo.example.org/index.php&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Éditer la configuration pour forcer le TLS puis redémarrer php-fpm&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;vi /usr/share/nginx/html/matomo/config/config.ini.php
[General]
salt = &amp;#34;aaaaaaaaaaaaaaaaaaaaaaaaaaaaa&amp;#34;
trusted_hosts[] = &amp;#34;matomo.example.org&amp;#34;
trusted_hosts[] = &amp;#34;blog.example.org&amp;#34;
force_ssl = 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Tuner la configuration de MariaDB pour faire plaisir à Matomo&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;vi /etc/my.cnf
max_allowed_packet=128M
systemctl restart mariadb
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Et pour finir, cette partie manque pas mal dans la doc (genre, pas documenté du tout) et me parait pourtant essentiel. Il faut compiler les modules manquants pour ajouter GeoIP 2 (libmaxminddb et MaxMind-DB-Reader-php).&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;wget https://github.com/maxmind/libmaxminddb/releases/download/1.3.2/libmaxminddb-1.3.2.tar.gz
tar xzf libmaxminddb-1.3.2.tar.gz &amp;amp;&amp;amp; cd libmaxminddb-1.3.2
./configure
make
sudo make install
sudo ldconfig
cd ..
wget https://github.com/maxmind/MaxMind-DB-Reader-php/archive/v1.4.1.tar.gz
tar xzf v1.4.1.tar.gz &amp;amp;&amp;amp; cd MaxMind-DB-Reader-php-1.4.1/ext
phpize
/configure
make
sudo make install
echo &amp;#34;extension=maxminddb.so&amp;#34; &amp;gt; /etc/php.d/30-maxminddb.ini
systemctl restart php-fpm
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2019/06/matomo.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tadaaaa !&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="configurer-hugo-pour-ajouter-le-code-matomo"&gt;Configurer Hugo pour ajouter le code Matomo
&lt;/h2&gt;&lt;p&gt;Cool ! Tout est correctement configuré dans Matomo. Maintenant, il faut terminer par ajouter le code de Hugo pour communiquer avec Matomo.&lt;/p&gt;
&lt;p&gt;Pour se faire, on peut utiliser le projet suivant, qui agit comme un thème complémentaire et étend le code généré par Hugo pour inclure le code de Matomo&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git submodule add https://github.com/holehan/hugo-components-matomo.git themes/matomo
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Dans cet exemple, j’ajoute simplement « matomo » comme 2ème thème de mon blog static dans le fichier de configuration &lt;strong&gt;config.toml&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;theme = [&amp;#34;aether&amp;#34;, &amp;#34;matomo&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Et je termine par ajouter la balise suivante dans le thème que j’utilise (ici aether, donc par exemple themes/aether/layouts/partials/head.html)&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{{ partial &amp;#34;matomo-tracking&amp;#34; . }}
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="vie-privée"&gt;Vie privée
&lt;/h2&gt;&lt;p&gt;Pour que tout soit vraiment terminé, il faut également ajouter la ligne suivante dans la page dédiée à la vie privée&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{{ matomo-optout }}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Et voilà le travail :)&lt;/p&gt;</description></item><item><title>Récap’ du premier jour de KubeCon Europe 2018</title><link>https://blog.zwindler.fr/2018/05/03/recap-du-premier-jour-de-kubecon-europe-2018/</link><pubDate>Wed, 02 May 2018 22:09:43 +0000</pubDate><guid>https://blog.zwindler.fr/2018/05/03/recap-du-premier-jour-de-kubecon-europe-2018/</guid><description>&lt;img src="https://blog.zwindler.fr/2018/05/kubecon.webp" alt="Featured image of post Récap’ du premier jour de KubeCon Europe 2018" /&gt;&lt;h2 id="premier-jour-de-kubecon-aujourdhui-"&gt;Premier jour de KubeCon aujourd’hui !
&lt;/h2&gt;&lt;p&gt;Hier soir, je suis arrivé à Copenhague pour participer à la KubeCon + CloudNativeCon Europe 2018. Vous ne savez pas de quoi il s’agit ? C’est LA conférence sur 3 jours, organisée par la CNCF, qui parle de Kubernetes et tout ce qui gravite autour.&lt;/p&gt;
&lt;p&gt;Ce n’est pas la première conf que je couvre sur le blog, j’avais déjà fais &lt;a class="link" href="https://blog.zwindler.fr/2018/01/26/recap-hack-it-n-2018/" &gt;un récapitulatif de la Hack-It-N 2018&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2018/05/20180502_090306.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;On est environ 4300&amp;hellip;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;La journée a commencée par des Keynotes, ponctuées par des petites interventions de Liz Rice et Kesley Hightower. La plupart étaient un peu humoristiques, mais j’ai retenu ceci parmis les keynotes du matin :&lt;/p&gt;
&lt;h3 id="où-en-sont-les-projets-de-la-cncf-"&gt;Où en sont les projets de la CNCF ?
&lt;/h3&gt;&lt;p&gt;&lt;em&gt;Keynote: CNCF Project Update - Liz Rice, Technology Evangelist, Aqua Security; Sugu Sougoumarane, CTO, PlanetScale Data; Colin Sullivan, Product Manager, Synadia Communications, Inc. &amp;amp; Andrew Jessup, Co-founder&amp;hellip;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Cette année, le moins que l’on puisse dire est que la CNCF a réellement pris de l’ampleur, en terme de projets notamment.&lt;/p&gt;
&lt;p&gt;L’ajout de plusieurs projets à l’état « Sandbox » :&lt;/p&gt;
&lt;p&gt;Rook (abstraction du stockage), One Policy Agent (politiques de sécurité pour l’ensemble de la stack), SPIFFE (langage de spec) &amp;amp; SPIRE (runtime env) qui a pour but d’établir un lien de confiance entre l’infrastructure et les workloads.&lt;/p&gt;
&lt;p&gt;Plusieurs projets sont à l’état « Incubator » :&lt;/p&gt;
&lt;p&gt;CoreDNS, Linkerd (service mesh), Envoy (distributed proxy), Prometheus, OpenTracing, Jaeger (distributed tracing), Fluentd (Splunk Entreprise connector + fluentd daemonset for kubernetes), Fluent bit (client lightweight un peu comme Beats d’Elastic), NATS (messaging), Container Network Interface (CNI), gRPC (client/server), Container runtime (Containerd / Kubernetes CRI), TUF (The Update Framework), Notary, Vitess (distribution de données sur MySQL avec Sharding/resharding automatic)&lt;/p&gt;
&lt;p&gt;La liste est vertigineuse.&lt;/p&gt;
&lt;p&gt;Kubernetes est également passé à l’état « Graduated » (premier projet CNCF a passé à l’état « production ready » pour la plupart des projets informatiques, pas seulement pour les tech enthousiasts et les early adopters.&lt;/p&gt;
&lt;p&gt;Si j’ai tout listé, c’est que l’ensemble de ces projets ont par la suite droit à une ou plusieurs confs et j’aurai l’occasion de revenir dessus si je participe aux confs en question.&lt;/p&gt;
&lt;h3 id="re-thinking-networking-for-microservices"&gt;Re-thinking Networking for Microservices
&lt;/h3&gt;&lt;p&gt;&lt;em&gt;Keynote: Re-thinking Networking for Microservices - Lew Tucker, VP/CTO Cloud Computing, Cisco Systems, Inc.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Le CTO Cloud Computing de Cisco est venu nous expliquer que maintenir l’ensemble des services permettant au microservice d’échanger avec le monde extérieur, c’est compliqué, et qu’il vaut mieux utiliser du service mesh (découpler tous les services de communication et externes aux services comme logging, api, auth, &amp;hellip;).&lt;/p&gt;
&lt;h3 id="rex-du-cern"&gt;REX du CERN
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://schd.ws/hosted_files/kccnceu18/d0/CERN.pdf" target="_blank" rel="noopener"
&gt;&lt;em&gt;Keynote: CERN Experiences with Multi-Cloud Federated Kubernetes&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;On ne présente pas le CERN (mais en fait on a quand même eu droit à une présentation ;-p ), mais au delà des travaux scientifiques, il faut des ops pour faire tourner la puissance de calcul brute.&lt;/p&gt;
&lt;p&gt;10000 hyperviseurs, des quantités de données très importantes (1 Po/s généré, 10 Go/s enregistré).&lt;/p&gt;
&lt;p&gt;Malgré cette force de frappe plus qu’honnête, lors de grosses campagnes de calculs scientifiques, il faut tirer parti de ressources externes (clouds, universités), ce qui induit la nécessité d’un outils permettant de fédérer des machines provenant de tous horizons.&lt;/p&gt;
&lt;p&gt;Le CERN utilise la fédération de cluster de Kubernetes, avec en plus l’outil &lt;a class="link" href="https://research.cs.wisc.edu/htcondor/" target="_blank" rel="noopener"
&gt;HTCondor&lt;/a&gt; qui étend l’API de Kubernetes et leur permet de lancer les jobs sur les machines (distantes ou pas) en fonction du job demandé.&lt;/p&gt;
&lt;h3 id="mini-talk-du-vp--chief-open-source-officer-de-vmware"&gt;Mini talk du VP &amp;amp; Chief Open Source Officer de VMware
&lt;/h3&gt;&lt;p&gt;&lt;em&gt;Keynote: From Innovation to Production - Dirk Hohndel, VP &amp;amp; Chief Open Source Officer, VMware&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Au delà du présentation plus que rapide des produits opensource sur lesquels contribue VMware, (Harbor, pivotal, BOSH), Dirk Hohndel a surtout utilisé les 5 minutes qu’il avait pour nous faire remarquer que les gens de l’audience sont en grande majorité des hommes blancs et qu’il y a un vrai problème de mixité dans nos métiers. J’ai regardé autour de moi, difficile de lui donner tort.&lt;/p&gt;
&lt;h2 id="et-les-vraies-conférences-ont-pu-commencer-"&gt;Et les vraies conférences ont pu commencer !
&lt;/h2&gt;&lt;h3 id="pourquoi-y-a-t-il-autant-de-runtimes-"&gt;Pourquoi y a-t-il autant de runtimes ?
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://schd.ws/hosted_files/kccnceu18/08/What%E2%80%99s%20Up%20With%20All%20the%20Container%20Runtimes.pdf" target="_blank" rel="noopener"
&gt;&lt;em&gt;Whats Up With All The Different Container Runtimes? - Ricardo Aravena, Branch Metrics&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Ricardo Aravena a brossé un tableau de l’évolution des containers runtimes sous Linux, puis a listé leurs avantages et leurs inconvénients. Un talk plutôt sympa en mode comparaisons et Pros/Cons sur les différents runtimes du marché.&lt;/p&gt;
&lt;h3 id="créer-et-gérer-une-communauté-open-source"&gt;Créer et gérer une communauté open source
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://schd.ws/hosted_files/kccnceu18/28/Building%20an%20Open%20Source%20Community%20-%20KubeCon%202018%20-%20Final.pptx" target="_blank" rel="noopener"
&gt;&lt;em&gt;Building an Open Source Community to Achieve Innovation-Through-Openness - Jonas Rosland, {code}&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Un talk « non technique » auquel j’avais vraiment envie d’aller. J’avais été assez surpris de remarquer l’ouverture de Dell EMC en 2015 avec l’équipe {code}, alors forcément, un REX sur comment gérer une communauté open source et le erreurs qui ont pu être faites m’intéressait forcément.&lt;/p&gt;
&lt;p&gt;Au final, le talk, même s’il est resté très haut niveau, a donné de bons conseils (planification de la roadmap, transparences, SIGs, focus sur les individus plus que les projets) et des ressources complémentaires (opensource.com guides, codeofconducts), qui n’existaient pas à l’époque où {code} s’est mise en place.&lt;/p&gt;
&lt;h3 id="rex-de-turbine-labs-a-propos-de-leur-migration-de-nginx-vers-envoy"&gt;REX de Turbine labs a propos de leur migration de nginx vers envoy
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://schd.ws/hosted_files/kccnceu18/a6/Turbine%20Labs_Move%20to%20Envoy%20Deck_V2.pdf" target="_blank" rel="noopener"
&gt;&lt;em&gt;Replacing NGINX with Envoy in a Traffic Control System - Mark McBride, Turbine Labs, Inc&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Le CEO de Turbine Labs nous a fait un REX sur un changement majeur qu’ils ont eu à réaliser sur leur produit : le changement de &lt;strong&gt;nginx&lt;/strong&gt; (limité pour leurs besoins spécifiques) pour &lt;strong&gt;Envoy&lt;/strong&gt;. Ce changement impliquait (1) d’écrire une partie des features manquantes dans Envoy (extensions maisons spécifiques à Turbine Labs pour leurs besoins propres) et la façon la plus progressives de migrer ce composant central dans leur architecture.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2018/05/20180502_141232-2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Si le talk était intéressant en soit (et permet d’avoir un premier aperçu de Envoy), c’était aussi l’occasion de se poser les bonnes questions pour tout migration, qu’elle qu’elle soit.&lt;/p&gt;
&lt;h3 id="rex-zalando-la-mise-à-jour-des-clusters-kubernetes"&gt;REX Zalando, la mise à jour des clusters Kubernetes
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://schd.ws/hosted_files/kccnceu18/18/2018-05-02%20Continuously%20Deliver%20your%20Kubernetes%20Infrastructure%20-%20KubeCon%202018%20Copenhagen.pdf" target="_blank" rel="noopener"
&gt;&lt;em&gt;Continuously Deliver your Kubernetes Infrastructure - Mikkel Larsen, Zalando SE&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Le site de e-commerce Zalando est quelque chose d’assez énorme. On ne s’en rend pas forcément compte, mais ce sont des dizaines de clusters Kubernetes qui sont nécessaires pour gérer la partie développement des nouvelles applications.&lt;/p&gt;
&lt;p&gt;Zalando est connue comme faisant partie des sociétés qui communiquent beaucoup sur leurs (bonnes) pratiques en terme d’IT. D’ailleurs l’amphi était plein à craquer :&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2018/05/20180502_144136_HDR.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Mikkel Larsen n’y a pas coupé et nous a méthodiquement expliqué pas à pas tout ce qui était mis en place pour assurer (1) des mises à jours des clusters Kubernetes complètement transparentes et quasi automatiques, et (2) des test ends to ends complet permettant de repérer des régressions mêmes subtiles.&lt;/p&gt;
&lt;p&gt;Côté technique, Zalando travaille sur AWS, sur plusieurs availability zones, avec des Stack Cloud Formations, 1 seule AMI pour toutes les VMs, des clusters etcd externalisés des nœuds Kubernetes (rendant les masters stateless, et donc plus facile à mettre à jour), &amp;hellip;&lt;/p&gt;
&lt;h3 id="the-route-to-rootless-container"&gt;The Route to rootless container
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://schd.ws/hosted_files/kccnceu18/08/route_to_rootless_slides.pdf" target="_blank" rel="noopener"
&gt;&lt;em&gt;The Route To Rootless Containers - Ed King, Pivotal &amp;amp; Julz Friedman, IBM&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Je ne vais pas le cacher, je suis allé pour le titre de la conf et je n’ai pas été déçus côté clins d’œils / memes (ni par le contenu, mais c’est toujours plus agréables quand les speakers sont bons).&lt;/p&gt;
&lt;p&gt;Si Ed King et Julz Friedman ont commencé par dire qu’ils étaient contents qu’autant de gens soient présents pour un « sujet aussi basique », le talk n’était clairement pas à destination de débutants !&lt;/p&gt;
&lt;p&gt;Point par point, ils ont expliqué quels technologies et couches de sécurités étaient disponibles, contre quoi elle protégeaient et ne protégeaient pas. Dans une seconde partie, ils ont ensuite expliqués comment réussir à faire fonctionner des containers exécutés côté hôte par un utilisateurs non privilégié mais privilégié au sein du container. Mais surtout, pourquoi ça fonctionne et pourquoi ça n’était pas grave.&lt;/p&gt;
&lt;p&gt;Une vraie surprise car j’avais mal compris la conf (je pensais qu’on allait expliquer comment faire pour que les applications ne soient pas root DANS le container).&lt;/p&gt;
&lt;h3 id="améliorer-la-sécurité-des-workloads-kubernetes-avec-la-virtualisation-matérielle"&gt;Améliorer la sécurité des workloads Kubernetes avec la virtualisation matérielle
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://schd.ws/hosted_files/kccnceu18/9f/2018-03%20KubeCon%20EU%20-%20Improving%20your%20Kubernetes%20Workload%20Security%20with%20Hardware%20Virtualization.pdf" target="_blank" rel="noopener"
&gt;&lt;em&gt;Improving your Kubernetes Workload Security with Hardware Virtualization - Fabian Deutsch, Red Hat &amp;amp; Samuel Ortiz, Intel&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Une conf’ amusante où deux projets dont le but est de faire tourner des workloads Kubernetes dans des machines virtuelles (Kata et KubeVirt), &lt;em&gt;soit disant non concurrents,&lt;/em&gt; ont été présentés en même temps.&lt;/p&gt;
&lt;p&gt;La subtilité, selon eux, c’est que :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kata containers est plutôt à destination des personnes qui souhaitent :
&lt;ul&gt;
&lt;li&gt;ajouter une couche de sécurité/ségrégation supplémentaire à la conteneurisation simple en ajoutant l’isolation via la virtualisation matérielle&lt;/li&gt;
&lt;li&gt;utiliser des kernels différents pour des containers différents sur un même groupe d’hôtes Kubernetes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Kubevirt permet l’adoption plus rapide de Kubernetes en permettant l’ajout des workload de type legacy « non containerisables » dans un cluster Kubernetes. Ces workloads deviennent des Pods qui tournent dans Kubernetes, ce qui a l’immense avantage d’ajouter toutes les fonctionnalités de Kubernetes sur les Pods, mais aussi la gestion de réseau et du stockage.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A vous de me dire si vous êtes convaincus qu’il faut absolument 2 projets ;-).&lt;/p&gt;
&lt;h2 id="et-vous-reprendrez-bien-quelques-keynotes"&gt;Et vous reprendrez bien quelques keynotes
&lt;/h2&gt;&lt;h3 id="anatomy-of-a-production-kubernetes-outage"&gt;Anatomy of a Production Kubernetes Outage
&lt;/h3&gt;&lt;p&gt;&lt;em&gt;Keynote: Anatomy of a Production Kubernetes Outage - Oliver Beattie, Head of Engineering, Monzo Bank&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Un postmortem d’un incident qui a bloqué la prod d’une banque « digital native ». Très bien présenté et d’autant plus intéressant que les « erreurs » qui ont été faites tendent plus de la malchance et de fausses bonnes idées que de l’erreur a proprement parler; il était très facile de m’imaginer à la place des équipes qui ont géré cet incident.&lt;/p&gt;
&lt;h3 id="container-native-dev-and-ops-experience"&gt;Container Native Dev and ops Experience
&lt;/h3&gt;&lt;p&gt;&lt;em&gt;Keynote: Container-Native Dev-and-ops Experience: It’s Getting Easier, Fast. - Ralph Squillace, Principal PM – Azure Container Platform, Microsoft&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Je ne vais pas mentir, je suis un peu passé à côté de cette keynote. Ralph Squillace nous a fait une démo en live de code qui se déploie, se débugue et tourne sur Kubernetes pour nous prouver à quel point c’est simple. Moui, ok.&lt;/p&gt;
&lt;p&gt;Au delà de ça, j’ai surtout remarqué que son PC était sous Ubuntu. Venant de quelqu’un qui bosse chez Microsoft (même si c’est une team Kubernetes), c’est quand même quelque chose qui aurait été impensable il y a quelques années ;-p&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2021/tweet_kubecon_2018.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h3 id="cloud-native-observability--security"&gt;Cloud native observability &amp;amp; security
&lt;/h3&gt;&lt;p&gt;&lt;em&gt;Keynote: CNCF End User Awards - Presented by Chris Aniszczyk, COO, Cloud Native Computing Foundation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Un résumé des annonces faites aujourd’hui par Google à l’occasion de la Kubecon.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;gVisor, un système révolutionnaire qui va résoudre tous les pb de sécurité des containers&lt;/li&gt;
&lt;li&gt;stackdriver, un outil de monitoring (observability) génial, qui rendre nos vies géniales&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Peut être que le format était mal choisi, mais je suis extrêmement sceptique et je vais attendre d’en savoir plus ;)&lt;/p&gt;
&lt;h3 id="prometheus-20"&gt;Prometheus 2.0
&lt;/h3&gt;&lt;p&gt;&lt;em&gt;Keynote: Prometheus 2.0 – The Next Scale of Cloud Native Monitoring - Fabian Reinartz, Software Engineer, Google&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Une tonne de slides pour nous brosser l’historique de Prometheus dans un premier temps, puis nous expliquer qu’un gros problème est arrivé, la TSDB de Prometheus était trop lente sur de très gros workloads. Déluge de slides pour nous montrer à quel point Prometheus 2 est bien meilleur.&lt;/p&gt;
&lt;p&gt;Maintenant c’est mieux.&lt;/p&gt;
&lt;h2 id="et-demain-"&gt;Et demain ?
&lt;/h2&gt;&lt;p&gt;Et bien demain rebelote (enfin si je poste pas très vite, on va déjà être demain&amp;hellip;).&lt;/p&gt;
&lt;p&gt;Si ça vous intéresse, vous pouvez suivre les confs que je vais voir sur &lt;a class="link" href="https://kccnceu18.sched.com/d.germain" target="_blank" rel="noopener"
&gt;kccnceu18.sched.com/d.germain&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Exposer des applications containerisées Kubernetes (nginx Ingress Controller)</title><link>https://blog.zwindler.fr/2018/03/06/exposer-des-applications-kubernetes-en-dehors-des-cloud-providers-nginx-ingress-controller/</link><pubDate>Tue, 06 Mar 2018 12:45:47 +0000</pubDate><guid>https://blog.zwindler.fr/2018/03/06/exposer-des-applications-kubernetes-en-dehors-des-cloud-providers-nginx-ingress-controller/</guid><description>&lt;img src="https://blog.zwindler.fr/2018/02/kubernetes_nginx.webp" alt="Featured image of post Exposer des applications containerisées Kubernetes (nginx Ingress Controller)" /&gt;&lt;h2 id="ingress-et-ingress-controller-versus-baremetal"&gt;Ingress et Ingress Controller versus Baremetal
&lt;/h2&gt;&lt;p&gt;Autant le dire tout de suite, cet article s’adresse surtout aux admins qui font du Kubernetes (K8s pour les intimes) et en plus qui font tout eux même. Mais comme je suis sympa j’explique tout et j’ai mis des schémas si vous êtes curieux ;-).&lt;/p&gt;
&lt;p&gt;Je vous plante le décors : &lt;a class="link" href="https://blog.zwindler.fr/recherche/?keyword=Kubernetes" &gt;vous avez suivi un de mes tutos sur Kubernetes&lt;/a&gt; et vous avez maintenant un cluster opérationnel en dehors des gros cloud providers (Amazon, Google, &amp;hellip;) et de leurs solutions clés en main (sur vos propres VMs et/ou serveurs physiques). Vous êtes super contents et il y a de quoi.&lt;/p&gt;
&lt;p&gt;Seulement voilà, maintenant que vous pouvez spawner des containers à tire larigot, vous voulez pouvoir y accéder depuis l’Internet.&lt;/p&gt;
&lt;h2 id="mais-là-comment-faire-"&gt;Mais là, comment faire ?
&lt;/h2&gt;&lt;p&gt;Le premier problème que vous risquez d’avoir est que les applications que vous déployez dans Kubernetes ne sont de base accessibles que DANS Kubernetes.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;On va être très très vite limité, niveau usecases.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Pour accéder à un container (ou faire discuter 2 containers comme un serveur web et une base de données), on passe par l’intermédiaire d’un objet appelé &lt;strong&gt;Service&lt;/strong&gt;. C’est cet objet qui permet le service discovery, le scaling aisé de nos applications et la résolution de nom au sein du cluster.&lt;/p&gt;
&lt;p&gt;Cependant, hors du cluster, pas de résolution de nom et/ou d’accès aux ressources par ce biais. Vous allez donc devoir passer par un DNS externe pour spécifier aux clients où sont vos applications et trouver un moyen de différencier quelle requête externe doit aboutir dans quelle container.&lt;/p&gt;
&lt;h2 id="accéder-aux-ressources"&gt;Accéder aux ressources
&lt;/h2&gt;&lt;p&gt;Le premier problème est donc de réussir à accéder à notre service. S’il est possible de créer des &lt;strong&gt;Services&lt;/strong&gt; (les points d’entrées de nos containers) de type &lt;strong&gt;LoadBalancer&lt;/strong&gt; sur AWS, Azure et GCP, qui ont l’avantage de permettre un accès direct au service depuis Internet (magie !), le mieux que l’on puisse faire par défaut sur un cluster Baremetal, c’est un &lt;strong&gt;Service&lt;/strong&gt; de type &lt;strong&gt;NodePort&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Si vous n’êtes pas chez un cloud provider comme nous l’avons dit au départ, vous allez vite vous rendre compte qu’il n’est pas trivial de présenter un container sur HTTP:80 ou HTTPS:443. Car par défaut, les &lt;strong&gt;Services NodePort&lt;/strong&gt; exposent nos container sur un port aléatoire compris entre 30000 et &lt;em&gt;3X000-je-sais-pas-combien&lt;/em&gt; (mate la précision) sur tous les nœuds du clusters.&lt;/p&gt;
&lt;p&gt;Il y a deux choses importantes dans cette dernière phrase :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;D’abord, on va se retrouver avec des URLs du type : &lt;code&gt;http://@IP_node:32251&lt;/code&gt; pour accéder à un serveur web depuis l’extérieur. Pas glop.&lt;/li&gt;
&lt;li&gt;Ensuite, pour un &lt;strong&gt;Service&lt;/strong&gt; de type &lt;strong&gt;NodePort&lt;/strong&gt; donné, le port alloué l’est sur TOUS les membres du cluster, que le container tourne dessus ou pas. Ça veut dire que &lt;em&gt;peu importe quel nœud Kubernetes on interroge&lt;/em&gt;, le 32251 répondra TOUJOURS sur le même container (les requêtes sont forwardé au bon nœud de manière transparente). Ça veut aussi dire qu’il n’y aura qu’un seul service qui aura le privilège d’utiliser le 32251 et aucun autre &lt;strong&gt;Service&lt;/strong&gt; ne pourra le réserver.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2018/02/Kubernetes1-1.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Clairement, c’est tout sauf idéal. On se retrouve donc à différencier les applications simplement avec les numéros des ports qu’on accède.&lt;/p&gt;
&lt;h2 id="le-dns"&gt;Le DNS
&lt;/h2&gt;&lt;p&gt;Et c’est pas fini&amp;hellip;&lt;/p&gt;
&lt;p&gt;Si on prend un cas simple : un site web à exposer sur Internet, on va souvent vouloir relier une URL humainement compréhensible au container qui gère ce site. Cependant cette notion n’a pas vraiment de sens avec Kubernetes.&lt;/p&gt;
&lt;p&gt;Vous ne savez pas &lt;em&gt;a priori&lt;/em&gt; &lt;strong&gt;où&lt;/strong&gt; va être instancié votre container dans le cluster (i.e. sur quelle machine et donc sur quelle IP). Ça sera un des nœuds du cluster, mais lequel ? D’autant que le &lt;strong&gt;Pod&lt;/strong&gt; bouge d’un nœud à l’autre au gré de la vie de l’application (&lt;em&gt;drain&lt;/em&gt; du node pour maintenance ou incident). Difficile donc de donner avec certitude un enregistrement DNS fiable dans le temps.&lt;/p&gt;
&lt;p&gt;Alors bien sûr, vous pouvez toujours fixer en dur l’adresse IP d’un des nœuds Kubernetes dans un enregistrement DNS mais cette méthode trouve très vite sa limite : le moindre arrêt du serveur cause la perte de l’accès à l’application, qui pourtant a probablement été redémarrée sur un nœud du cluster.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2018/02/Kubernetes2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Vous me pardonnerez ce « comic » en mode Draw.io. On s’amuse comme on peut ;-)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A quoi ça sert d’avoir un cluster dans ce cas là ?&lt;/p&gt;
&lt;p&gt;Vous aurez peut être alors l’idée d’utiliser un moyen automatique (&lt;a class="link" href="https://blog.zwindler.fr/2014/09/22/mise-a-jour-de-votre-dns-chez-ovh-avec-dynhost/" &gt;genre ce script qui utilise Dynhost, au hasard&lt;/a&gt;) pour mettre à jour le DNS dès que l’application change de nœud. Mais c’est clairement de la bidouille et on se retrouvera avec pleins d’utilisateurs dont les caches DNS ne sont pas à jour.&lt;/p&gt;
&lt;h2 id="la-méthode-simple"&gt;La méthode simple
&lt;/h2&gt;&lt;p&gt;On peut quand même bosser avec ce qu’on a et présenter à nos gentils utilisateurs des URLs user-friendly sans trop de difficultés, dans la majorité des cas.&lt;/p&gt;
&lt;p&gt;La première chose qu’on peut faire c’est simplement mettre en face de notre cluster Kubernetes un bête reverse proxy et/ou répartiteur de charge. Après tout, c’est ni plus ni moins ce que font AWS et autre avec leur implémentation du &lt;strong&gt;Service&lt;/strong&gt; de type &lt;strong&gt;Loadbalancer&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;L’ensemble des requêtes HTTP(S) sont donc dirigées vers le reverse proxy, qui se charge de masquer la complexité de l’URL (l’aiguillage via des ports chelous et le maintiens à jour de l’ensemble des nœud du cluster qu’on arrive à contacter) à l’utilisateur final.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2018/02/Kubernetes3.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Personnellement, &lt;strong&gt;pour mes projets perso&lt;/strong&gt;, ça m’ennuie d’avoir un composant externe au cluster K8s pour faire ça. J’ai déjà investi dans un cluster pour rendre mes applications hautement disponibles (ce qui est déjà overkill), je ne vais pas en plus :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;soit ajouter un SPOF en ajoutant une seule machine pour faire mon reverse proxy&lt;/li&gt;
&lt;li&gt;soit ajouter un cluster de machines rien que pour le reverse proxy&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="et-les-ingress-dans-tout-ça-"&gt;Et les Ingress dans tout ça ?
&lt;/h2&gt;&lt;p&gt;Donc on l’a vu, tout ça c’est pas super propre et on ajoute potentiellement un SPOF, mais ça « marche ». Mais on peut faire mieux :)&lt;/p&gt;
&lt;p&gt;Si vous manipulez un peu Kubernetes, vous avez donc peut être vu qu’il existe un autre type d’objet qui s’appelle les &lt;strong&gt;Ingress&lt;/strong&gt; et les &lt;strong&gt;Ingress Controllers&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Pour ceux qui n’ont pas eu le temps de potasser, un &lt;strong&gt;Ingress&lt;/strong&gt; est une règle qui permet de relier une URL à un &lt;strong&gt;Service&lt;/strong&gt; et un &lt;strong&gt;Ingress Controller&lt;/strong&gt; est un composant qui permet de piloter un reverse-proxy pour implémenter cette règle. C’est &lt;em&gt;LA&lt;/em&gt; méthode propre pour traduire une URL provenant d’un client en requête interne dans le cluster K8s pour atteindre le bon service.&lt;/p&gt;
&lt;p&gt;L’&lt;strong&gt;Ingress Controller&lt;/strong&gt; le plus utilisé est probablement celui pour nginx, mais il existe des &lt;strong&gt;Ingress Controllers&lt;/strong&gt; pour &lt;a class="link" href="https://doc.traefik.io/traefik/reference/install-configuration/providers/kubernetes/kubernetes-ingress/" target="_blank" rel="noopener"
&gt;traefik&lt;/a&gt; ou ha-proxy (ou autre) qui ont été développés et qui sont plus ou moins aboutis.&lt;/p&gt;
&lt;h2 id="helm"&gt;Helm
&lt;/h2&gt;&lt;p&gt;Helm c’est super. &lt;a class="link" href="https://blog.zwindler.fr/2018/02/06/se-simplifier-kubernetes-helm-charts/" &gt;J’en ai parlé dans un autre article il n’y a pas longtemps&lt;/a&gt;, c’est un genre de gestionnaire d’application pour Kubernetes et ça simplifie grandement son utilisation. Mais il faut être honnête, ce n’est pas toujours une super idée&amp;hellip;&lt;/p&gt;
&lt;p&gt;Déjà, la documentation sur &lt;a class="link" href="https://github.com/kubernetes/ingress-nginx" target="_blank" rel="noopener"
&gt;la page d’accueil du Chart est super light&lt;/a&gt;. Si vous ne trouvez pas d’infos sur comment le déployer, c’est normal, c’est parce qu’il faut aller dans le dossier « deploy » du dépôt pour &lt;a class="link" href="https://github.com/kubernetes/ingress-nginx/tree/master/deploy" target="_blank" rel="noopener"
&gt;trouver la doc d’installation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Du coup, confiant, j’y suis allé avant d’avoir trouvé cette page, et je me suis pris ma première claque ;-)&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;helm install stable/nginx-ingress --name nginx-ingress
NAME: nginx-ingress
LAST DEPLOYED: Sun Feb 25 16:24:11 2018
NAMESPACE: default
STATUS: DEPLOYED
[...]
NOTES:
The nginx-ingress controller has been installed.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Oh cool, ça marché déjà ?&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-ingress-controller-7c6f79d45c-jwqkp 0/1 CrashLoopBackOff 14 47m
nginx-ingress-default-backend-6664bc64c9-2zpg4 1/1 Running 0 47m
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ah ben non en fait parce que je suis en RBAC (K8s &amp;gt; 1.7.0) ! Il faut créer des autorisations et un compte de service pour l’&lt;strong&gt;Ingress Controller&lt;/strong&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;kubectl logs nginx-ingress-controller-7c6f79d45c-jwqkp
-------------------------------------------------------------------------------
NGINX Ingress controller
Release: 0.10.2
Build: git-fd7253a
Repository: https://github.com/kubernetes/ingress-nginx
-------------------------------------------------------------------------------
I0225 16:11:13.374547 7 flags.go:159] Watching for ingress class: nginx
I0225 16:11:13.376659 7 main.go:181] Creating API client for https://10.233.0.1:443
I0225 16:11:13.438774 7 main.go:193] Running in Kubernetes Cluster version v. (v1.9.0+coreos.0) - git (clean) commit 1b69a2a6c01194421b0aa17747a8c1a81738a8dd - platform linux/amd64
F0225 16:11:13.442654 7 main.go:80] &amp;amp;#x2716; It seems the cluster it is running with Authorization enabled (like RBAC) and there is no permissions for the ingress controller. Please check the configuration
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ok, pas grave&amp;hellip; On supprime et on recommence !&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;helm list
NAME REVISION UPDATED STATUS CHART NAMESPACE
nginx-ingress 1 Sun Feb 25 16:24:11 2018 DEPLOYED nginx-ingress-0.9.1 default
helm delete nginx-ingress
release &amp;#34;nginx-ingress&amp;#34; deleted
helm install stable/nginx-ingress --name nginx-ingress --set rbac.create=true
[...]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Bon, ne vous fatiguez pas. Ça aura l’air de marcher mais en fait, le &lt;em&gt;nginx-ingress-controller&lt;/em&gt; se base sur un &lt;strong&gt;Service&lt;/strong&gt; de type&amp;hellip; &lt;strong&gt;Loadbalancer&lt;/strong&gt; ! Si vous n’êtes pas sur AWS, GCE ou Azure, votre &lt;strong&gt;Service&lt;/strong&gt; restera indéfiniment à l’état &lt;em&gt;Pending&lt;/em&gt; en attente d’une IP publique ;-)&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-ingress-controller LoadBalancer 10.233.52.122 80:30215/TCP,443:31438/TCP 41m
nginx-ingress-default-backend ClusterIP 10.233.29.8 80/TCP 41m
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="donc-on-laisse-tomber-helm-pour-cette-fois"&gt;Donc on laisse tomber Helm pour cette fois
&lt;/h2&gt;&lt;p&gt;En réalité, le mieux c’est de repartir sur le projet nginx Ingress Controller, qui donne (pour qui trouve la documentation) la bonne marche à suivre pour instancier rapidement son &lt;strong&gt;Ingress Controller&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/kubernetes/ingress-nginx" target="_blank" rel="noopener"
&gt;github.com/kubernetes/ingress-nginx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/kubernetes/ingress-nginx/blob/master/docs/deploy/index.md#bare-metal" target="_blank" rel="noopener"
&gt;github.com/kubernetes/ingress-nginx/blob/master/docs/deploy/index.md#bare-metal&lt;/a&gt; (lien mis à jour le 08/10/2018)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Partie à exécuter dans tous les cas&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/cb876766897806ed60b1f2f36564d5f3af8a18c8/deploy/namespace.yaml \
| kubectl apply -f -
curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/cb876766897806ed60b1f2f36564d5f3af8a18c8/deploy/default-backend.yaml \
| kubectl apply -f -
curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/cb876766897806ed60b1f2f36564d5f3af8a18c8/deploy/configmap.yaml \
| kubectl apply -f -
curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/cb876766897806ed60b1f2f36564d5f3af8a18c8/deploy/tcp-services-configmap.yaml \
| kubectl apply -f -
curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/cb876766897806ed60b1f2f36564d5f3af8a18c8/deploy/udp-services-configmap.yaml \
| kubectl apply -f -
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Partie spécifique à ajouter pour déployer l’&lt;strong&gt;Ingress Controller&lt;/strong&gt; dans un environnement de type RBAC (K8s &amp;gt;= 1.7)&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/cb876766897806ed60b1f2f36564d5f3af8a18c8/deploy/rbac.yaml \
| kubectl apply -f -
curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/cb876766897806ed60b1f2f36564d5f3af8a18c8/deploy/with-rbac.yaml \
| kubectl apply -f -
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Partie spécifique à ajouter en dernier, dans le cas où on est sur un cluster baremetal (i.e. en dehors des cloud providers).&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/cb876766897806ed60b1f2f36564d5f3af8a18c8/deploy/provider/baremetal/service-nodeport.yaml \
| kubectl apply -f -
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="ok-on-a-quoi-maintenant-"&gt;OK, on a quoi maintenant ?
&lt;/h2&gt;&lt;p&gt;Et bien, c’est pas encore fini malheureusement&amp;hellip; En fait, tel quel, le dernier fichier YAML qu’on a créé nous créé un service de type NodePort&amp;hellip;&lt;/p&gt;
&lt;p&gt;Oui vous l’avez compris : retour à la case départ ! Nous avons un composant reverse proxy intégré à K8s, mais qui est en écoute sur un port 3XXXX au lieu du 80, et 3YYYY au lieu du 443. Là encore, on aurait pas du tout ce problème si on était chez AWS, car un service de type Loadbalancer (avec une IP publique) aurait été créé pour nous.&lt;/p&gt;
&lt;p&gt;Rassurez vous, il y a des solutions, moyennant un peu de bidouille. J’en ai trouvé 2 dans notre cas.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://medium.com/@olegsmetanin/how-to-setup-baremetal-kubernetes-cluster-with-kubespray-and-deploy-ingress-controller-with-170cdb5ac50d" target="_blank" rel="noopener"
&gt;blog.will3942.com/nginx-kubernetes-bare-metal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&amp;ldquo;&lt;a class="link" href="https://medium.com/@olegsmetanin/how-to-setup-baremetal-kubernetes-cluster-with-kubespray-and-deploy-ingress-controller-with-170cdb5ac50d%3e" target="_blank" rel="noopener"
&gt;https://medium.com/@olegsmetanin/how-to-setup-baremetal-kubernetes-cluster-with-kubespray-and-deploy-ingress-controller-with-170cdb5ac50d&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pour faire simple, la première nécessite juste de modifier unfichier de configuration de Kubernetes pour autoriser le composant &lt;strong&gt;NodePort&lt;/strong&gt; à utiliser des ports en dessous de la limite arbitraire des 30000. Je ne suis pas fan du tout de cette solution mais elle « fonctionne » donc je vous laisse juger.&lt;/p&gt;
&lt;p&gt;En revanche, je trouve la seconde bien plus élégante quoique peut être un peu moins flexible. On peut modifier le &lt;strong&gt;Service&lt;/strong&gt; de l’&lt;strong&gt;Ingress Controller&lt;/strong&gt; en y listant toutes les adresses IPs de tous vos serveurs K8s. Ce n’est pas super confortable dans le cas où ils changent souvent, mais ça à l’effet qu’on recherche, à savoir ouvrir les ports 80 et 443 et rediriger l’ensemble des requêtes HTTP vers les &lt;strong&gt;Ingress&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;La modification à réaliser se situe au niveau du champ &lt;strong&gt;externalIPs&lt;/strong&gt; :&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/cb876766897806ed60b1f2f36564d5f3af8a18c8/deploy/provider/baremetal/service-nodeport.yaml -o ingress-nginx-service-nodeport.yaml
vi ingress-nginx-service-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: ingress-nginx
namespace: ingress-nginx
spec:
type: NodePort
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
- name: https
port: 443
targetPort: 443
protocol: TCP
externalIPs:
- 10.0.0.1
- 10.0.0.2
- 10.0.0.3
selector:
app: ingress-nginx
kubectl apply -f ingress-nginx-service-nodeport.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Normalement après application de la modification vous devriez avoir la joie (oui oui, carrément) de voir apparaitre sur tous les serveurs (que vous aurez listés) les ports 80 et 443 en écoute !!&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ss -lnt
LISTEN 0 128 8.8.8.8:80 *:*
LISTEN 0 128 8.8.8.8:443 *:*
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="super-et-maintenant-cest-bon-mon-container-nginx-est-accessible-"&gt;Super, et maintenant c’est bon, mon container nginx est accessible ?
&lt;/h2&gt;&lt;p&gt;Ah ben non, toujours pas. On a configuré l’&lt;strong&gt;Ingress Controller&lt;/strong&gt;, mais ça fait un moment qu’on parle plus des &lt;strong&gt;Ingress&lt;/strong&gt;&amp;hellip;&lt;/p&gt;
&lt;p&gt;Comme je l’ai dis plus haut, les &lt;strong&gt;Ingress&lt;/strong&gt; sont donc les règles de notre reverse proxy, permettant de mapper les URLs entrant dans le cluster sur les ports 80 et 443 (ou autre si on en définit d’autres) vers les &lt;strong&gt;Services&lt;/strong&gt; définis dans le K8s.&lt;/p&gt;
&lt;p&gt;Heureusement c’est partie est assez simple à configurer. Voici l’exemple minimaliste d’une application dont le &lt;strong&gt;Service&lt;/strong&gt; &lt;em&gt;toto-app-svc&lt;/em&gt; écoute sur le port 8080 à l’intérieur du cluster Kubernetes. Ce service est maintenant accessible pour toute requête HTTP aboutissant sur un des noeuds du cluster et accédé avec l’URL toto.zwindler.fr.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cat nginx-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
name: nginx-ingress
spec:
rules:
- host: toto.zwindler.fr
http:
paths:
- path: /
backend:
serviceName: toto-app-svc
servicePort: 8080
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Point intéressant à noter, il n’est plus nécessaire de faire du &lt;strong&gt;NodePort&lt;/strong&gt; maintenant, on peut se contenter des &lt;strong&gt;ClusterIP&lt;/strong&gt; par défaut. En effet, l’&lt;strong&gt;Ingress Controller&lt;/strong&gt; étant interne au cluster K8s, il sait résoudre les Services sans passer par un port crado en 30000. Autant ne plus les exposer donc et bannir les &lt;strong&gt;NodePorts&lt;/strong&gt; à présent !&lt;/p&gt;
&lt;p&gt;Bon vous avez vu ? Cette dernière étape n’est pas la plus dure qu’on ait fait !&lt;/p&gt;
&lt;p&gt;Et maintenant, à vos &lt;strong&gt;Ingress&lt;/strong&gt; !&lt;/p&gt;</description></item></channel></rss>