<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr" xml:base="https://vincent.bernat.ch">
  <title>Vincent Bernat — Distribution &gt; NixOS</title>
  <link href="https://vincent.bernat.ch/fr/blog/atom-distribution-nixos.xml" rel="self"/>
  <link href="https://vincent.bernat.ch/fr/blog#tag-distribution-nixos" rel="alternate"/>
  <id>http://www.luffy.cx/fr/blog/atom-distribution-nixos.xml/</id>
  <updated>2026-11-04T06:45:13Z</updated>

  <entry>
    <title type="html">Des statistiques sans robots avec GoatCounter auto-hébergé sur NixOS</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-goatcounter" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-goatcounter#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-09-20T19:44:50Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-goatcounter.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>En 2016, j'ai <a href="https://github.com/vincentbernat/vincent.bernat.ch/commit/4c3be65404031bf6638c1ea3d891a3ea58e0a0fc" title="layout: remove Google Analytics">retiré Google Analytics</a> de ce blog pour ne pas me
rendre complice de la plus grande machine à aspirer les données personnelles. À
la place, j'analysais les journaux de mes serveurs<sup id="fnref:scrub"><a class="footnote-ref" href="#fn:scrub">1</a></sup> avec <a href="https://goaccess.io/" title="GoAccess: Visual Web Log Analyzer">GoAccess</a>.
Depuis quelques années, les statistiques ne veulent plus rien dire, malgré mes
tentatives pour filtrer les robots : <a href="https://herman.bearblog.dev/the-great-scrape/" title="The Great Scrape">les robots pilleurs des IA</a>
gonflent le nombre de visiteurs à environ 2 000 par jour. J'ai fini par adopter
<a href="https://www.goatcounter.com/" title="GoatCounter: open source web analytics">GoatCounter</a>, une plateforme libre de mesure d'audience, respectueuse de la
vie privée. J'ai remplacé le client JavaScript pour filtrer les robots de façon
plus agressive et ajouté une solution de repli en CSS. Pour gagner en fiabilité,
j'ai écrit un proxy local, installé sur chacun des cinq serveurs web qui
hébergent ce blog. La suite de cet article détaille comment ces éléments
s'articulent et comment je déploie l'ensemble sur NixOS. ❄️</p>
<div class="toc">
<ul>
<li><a href="#pourquoi-goatcounter">Pourquoi GoatCounter ?</a></li>
<li><a href="#client-javascript-sur-mesure">Client JavaScript sur mesure</a></li>
<li><a href="#proxy-local">Proxy local</a></li>
<li><a href="#deploiement-sur-nixos">Déploiement sur NixOS</a><ul>
<li><a href="#deployer-des-applications-dans-des-conteneurs">Déployer des applications dans des conteneurs</a></li>
<li><a href="#serveur-goatcounter">Serveur GoatCounter</a></li>
<li><a href="#proxy-goatcounter">Proxy GoatCounter</a></li>
<li><a href="#sauvegarde-de-la-base-de-donnees-sqlite-avec-litestream">Sauvegarde de la base de données SQLite avec Litestream</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="pourquoi-goatcounter">Pourquoi GoatCounter ?</h1>
<p>GoatCounter ne <a href="https://www.goatcounter.com/help/sessions#technical-details-11181" title="GoatCounter documentation: sessions">collecte aucune donnée personnelle</a> : au
lieu de stocker l'adresse IP du lecteur ou de s'appuyer sur des cookies, il crée
un identifiant de session valide 8 heures à partir de l'agent utilisateur et de
l'adresse IP. Ses fonctionnalités sont modestes, mais suffisantes pour un blog.
Pour un aperçu de l'interface, l'auteur de GoatCounter propose une <a href="https://stats.arp242.net" title="Analytics for www.arp242.net">instance
publique</a> pour <a href="https://www.arp242.net" title="Personal website of Martin Tournoij">son site</a>. Une version en ligne permet de
l'essayer avant de l'héberger soi-même. Avec un seul binaire et une base de
données SQLite, GoatCounter est l'une des solutions auto-hébergées les plus
légères. Parmi les alternatives respectueuses de la vie privée figurent, par
ordre croissant de complexité, <a href="https://umami.is/" title="Umami: Privacy-First Analytics Platform">Umami</a>, <a href="https://plausible.io/" title="Plausible Analytics: Simple, privacy-friendly Google Analytics alternative">Plausible</a> et <a href="https://rybbit.com/" title="Rybbit: Cookieless Google Analytics Replacement">Rybbit</a>.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 889px"><span class="lf-media-inner" style="padding-bottom: 96.850%"><img alt="Tableau de bord de GoatCounter montrant les statistiques de mon blog sur la&#10;semaine passée, dont l'article sur le protocole Spanning Tree avec 1 224 vues,&#10;ainsi que les statistiques sur les navigateurs (52 % pour Chrome, 32 % pour&#10;Firefox)" src="https://media-bernat-ch.b-cdn.net/images/goatcounter@1x.a055a7216208d0.png" srcset="https://media-bernat-ch.b-cdn.net/images/goatcounter@1x.a055a7216208d0.png 1117w,https://media-bernat-ch.b-cdn.net/images/goatcounter@2x.12641dd59eef89.png 1778w" sizes="auto, (max-width: 889px) 100vw, 889px" width="889" height="861" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAH8AAAB7AQMAAABn4s1GAAAAA1BMVEX5+Pfi03mPAAAAGElEQVR42mNgGAWjYBSMglEwCkbBKMAJAAgrAAG9UB6fAAAAAElFTkSuQmCC)"/></span></div></figure>
<h1 id="client-javascript-sur-mesure">Client JavaScript sur mesure</h1>
<p>GoatCounter fournit un <a href="https://github.com/arp242/goatcounter/blob/main/public/count.js" title="GoatCounter's JavaScript client">petit client JavaScript</a> :
2 189 octets une fois minifié et compressé avec gzip. Il embarque des
fonctionnalités dont je ne me sers pas : un compteur de visites, le suivi des
clics, divers réglages, etc. Je le remplace par cette fonction pour enregistrer
une visite :</p>
<div class="language-javascript codehilite"><pre><span/><code><span class="kd">const</span><span class="w"> </span><span class="nx">count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">event</span><span class="p">,</span><span class="w"> </span><span class="nx">title</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">URLSearchParams</span><span class="p">({</span>
<span class="w">    </span><span class="nx">p</span><span class="o">:</span><span class="w"> </span><span class="nx">event</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">location</span><span class="p">.</span><span class="nx">pathname</span><span class="p">,</span>
<span class="w">    </span><span class="nx">t</span><span class="o">:</span><span class="w"> </span><span class="nx">title</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">title</span><span class="p">,</span>
<span class="w">    </span><span class="nx">r</span><span class="o">:</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">referrer</span><span class="p">,</span>
<span class="w">    </span><span class="nx">q</span><span class="o">:</span><span class="w"> </span><span class="nx">location</span><span class="p">.</span><span class="nx">search</span><span class="p">,</span>
<span class="w">    </span><span class="nx">s</span><span class="o">:</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">clientWidth</span><span class="p">,</span>
<span class="w">    </span><span class="nx">e</span><span class="o">:</span><span class="w"> </span><span class="o">!!</span><span class="nx">event</span><span class="p">,</span>
<span class="w">    </span><span class="nx">rnd</span><span class="o">:</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">().</span><span class="nx">toString</span><span class="p">(</span><span class="mf">36</span><span class="p">).</span><span class="nx">slice</span><span class="p">(</span><span class="mf">2</span><span class="p">,</span><span class="w"> </span><span class="mf">7</span><span class="p">),</span>
<span class="w">  </span><span class="p">});</span>
<span class="w">  </span><span class="nx">fetch</span><span class="p">(</span><span class="sb">`/count?</span><span class="si">${</span><span class="nx">params</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">keepalive</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">}).</span><span class="k">catch</span><span class="p">(()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{});</span>
<span class="p">};</span>
</code></pre></div>

<p>Pour filtrer les robots<sup id="fnref:bot-filter"><a class="footnote-ref" href="#fn:bot-filter">2</a></sup>, je vais plus loin en exigeant une
interaction de l'utilisateur, une idée reprise de <a href="https://bearblog.dev/" title="Bear Blog">Bear Blog</a>.</p>
<div class="language-javascript codehilite"><pre><span/><code><span class="kd">let</span><span class="w"> </span><span class="nx">sendHit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">(</span><span class="nx">sendHit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{},</span><span class="w"> </span><span class="nx">count</span><span class="p">());</span>
<span class="p">[</span><span class="s2">"touchmove"</span><span class="p">,</span><span class="w"> </span><span class="s2">"mousemove"</span><span class="p">,</span><span class="w"> </span><span class="s2">"keydown"</span><span class="p">,</span><span class="w"> </span><span class="s2">"pointerdown"</span><span class="p">].</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">eventName</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span>
<span class="w">  </span><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="nx">eventName</span><span class="p">,</span><span class="w"> </span><span class="nx">sendHit</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">once</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w">    </span><span class="nx">passive</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w">  </span><span class="p">}),</span>
<span class="p">);</span>
</code></pre></div>

<p>Si un lecteur a désactivé JavaScript dans son navigateur, j'enregistre la visite
à l'aide d'une image CSS. La pseudo-classe <code>:hover</code> ne la charge qu'après une
interaction, une autre astuce <a href="https://herman.bearblog.dev/how-bear-does-analytics-with-css/" title="How Bear does analytics with CSS">piquée à Bear Blog</a>. Environ 2 % de mes
visiteurs entrent dans cette catégorie<sup id="fnref:referrer"><a class="footnote-ref" href="#fn:referrer">3</a></sup>.</p>
<div class="language-html codehilite"><pre><span/><code><span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="p">&lt;</span><span class="nt">html</span> <span class="na">lang</span><span class="o">=</span><span class="s">"en"</span> <span class="na">class</span><span class="o">=</span><span class="s">"nojs"</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">script</span><span class="p">&gt;</span>
<span class="w">      </span><span class="c1">// Le code JavaScript de ce blog requiert ES6</span>
<span class="w">      </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="s2">"noModule"</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nx">HTMLScriptElement</span><span class="p">.</span><span class="nx">prototype</span><span class="p">)</span>
<span class="w">        </span><span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="s2">"nojs"</span><span class="p">);</span>
<span class="w">    </span><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
  <span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
  <span class="cm">&lt;!-- ... --&gt;</span>
    <span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;</span>
<span class="w">      </span><span class="p">.</span><span class="nc">nojs</span><span class="w"> </span><span class="nt">body</span><span class="p">:</span><span class="nd">hover</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">border-width</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="w">        </span><span class="k">border-image</span><span class="p">:</span><span class="w"> </span><span class="nb">url</span><span class="p">(</span><span class="s1">'/count?p=/en/blog/2026-kpi-goodhart&amp;t=Building...&amp;r=NoJS&amp;e=false'</span><span class="p">);</span>
<span class="w">      </span><span class="p">}</span>
<span class="w">    </span><span class="p">&lt;/</span><span class="nt">style</span><span class="p">&gt;</span>
  <span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</code></pre></div>

<p>Là où GoAccess annonçait environ 2 000 visiteurs par jour, GoatCounter compte
moins de 200 humains<sup id="fnref:rss"><a class="footnote-ref" href="#fn:rss">4</a></sup>. Je suppose que ces pilleurs privilégient le moindre
effort : si le contenu est accessible sans obstacle, comme sur ce blog, ils ne
vont pas lancer une usine à gaz qui déclencherait une page vue. Même les robots
qui exécutent JavaScript, comme <a href="https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics" title="Google Search Central: Understand the JavaScript SEO basics">Googlebot avec son Chromium sans
interface</a>, <a href="https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading#:~:text=Google%20Search%20does%20not%20interact%20with%20your%20page" title="Google Search Central: Fix lazy-loaded content">n'interagissent pas avec la page</a>
et ne déclenchent jamais les événements que j'écoute. La « preuve d'humanité »
par l'interaction que j'utilise me semble pouvoir durer<sup id="fnref:css-test"><a class="footnote-ref" href="#fn:css-test">5</a></sup>.</p>
<div class="admonition">
<p class="admonition-title">Mise à jour (11.2026)</p>
<p>J'ai ajouté un filtre supplémentaire pour le compteur "NoJS" afin
d'exclure les robots qui récupèrent l'URL de la page. Le code suivant
vérifie si le paramètre <code>r</code> de la requête vaut <code>NoJS</code> et si la valeur de
l'<a href="https://developer.mozilla.org/fr/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Dest" title="En-tête Sec-Fetch-Dest sur MDN">en-tête <code>Sec-Fetch-Dest</code></a> n'est pas <code>image</code>. Dans ce
cas, Caddy renvoie une réponse 204 vide. Sinon, il transmet la requête à
<em>GoatCounter</em>.</p>
<div class="language-caddyfile codehilite"><pre><span/><code><span class="k">route</span><span class="w"> </span>/count<span class="w"> </span><span class="p">{</span>
<span class="w">  </span>log_skip
<span class="w">  </span><span class="nd">@nojsbot</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span>query<span class="w"> </span>r=NoJS
<span class="w">    </span>not<span class="w"> </span>header<span class="w"> </span>Sec-Fetch-Dest<span class="w"> </span>image
<span class="w">  </span><span class="p">}</span>
<span class="w">  </span><span class="k">respond</span><span class="w"> </span><span class="nd">@nojsbot</span><span class="w"> </span><span class="m">204</span>
<span class="w">  </span><span class="k">reverse_proxy</span><span class="w"> </span>127.0.0.3:8087<span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="c1"># …</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

</div>
<h1 id="proxy-local">Proxy local</h1>
<p>Cinq serveurs <del>à travers le monde</del> <ins>en Europe et en Amérique du
Nord</ins> servent le contenu de ce site, mais GoatCounter ne tourne que sur
l'un d'eux. Pour ne pas perdre la trace des visiteurs quand GoatCounter est
indisponible, j'exécute un proxy local qui écoute sur le même point de
terminaison <code>/count</code>. Sur chaque serveur, il conserve les visites en mémoire,
avec un tampon assez grand pour tenir plusieurs jours en cas d'indisponibilité.
Il les envoie par lots au serveur amont via le <a href="https://www.goatcounter.com/help/api" title="GoatCounter documentation: API">point de terminaison authentifié
<code>/api/v0/count</code></a>.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 836px"><span class="lf-media-inner" style="padding-bottom: 58.852%"><object width="836" height="492" type="image/svg+xml" data="https://media-bernat-ch.b-cdn.net/images/luffy-web-servers.be5e1b31615943.svg" class="lf-media">🖼 Serveurs sur une carte. web02 est à Paris, web03 à Helsinki, web04 à
Nuremberg, web05 à Ashburn, web06 à
Chicago.</object></span></div></figure>
<p>J'ai proposé le code du proxy dans la <a href="https://github.com/arp242/goatcounter/pull/909" title="PR #909: add a proxy to forward page counts"><em>pull request</em> #909</a>. Le
mainteneur de GoatCounter a refusé de maintenir cet amas de code pour un cas
d'usage aussi niche. Développant moi aussi des logiciels libres, j'adopte
souvent la même position pour mes propres projets : l'effort ponctuel d'un
contributeur peut se transformer en engagement sur le long terme pour le
mainteneur.</p>
<p>J'expose le point de terminaison du proxy sur le domaine de ce site pour
échapper aux bloqueurs de publicité. Cela donne l'impression que je ne respecte
pas le choix du lecteur, mais comme GoatCounter respecte la vie privée, je
trouve cela acceptable.</p>
<div class="language-nginx-configuration-file codehilite"><pre><span/><code><span class="k">location</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">/count</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kn">access_log</span><span class="w"> </span><span class="no">off</span><span class="p">;</span>
<span class="w">  </span><span class="kn">proxy_pass</span><span class="w"> </span><span class="s">http://127.0.0.3:8087/count</span><span class="p">;</span>
<span class="w">  </span><span class="kn">proxy_pass_request_headers</span><span class="w"> </span><span class="no">off</span><span class="p">;</span>
<span class="w">  </span><span class="kn">proxy_set_header</span><span class="w"> </span><span class="s">Accept-Language</span><span class="w"> </span><span class="nv">$http_accept_language</span><span class="p">;</span>
<span class="w">  </span><span class="kn">proxy_set_header</span><span class="w"> </span><span class="s">User-Agent</span><span class="w"> </span><span class="nv">$http_user_agent</span><span class="p">;</span>
<span class="w">  </span><span class="kn">proxy_set_header</span><span class="w"> </span><span class="s">X-Real-Ip</span><span class="w"> </span><span class="nv">$remote_addr</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<h1 id="deploiement-sur-nixos">Déploiement sur NixOS</h1>
<p>Mes serveurs web tournent sous <a href="https://nixos.org/" title="Nix &amp; NixOS: Declarative builds and deployments">NixOS</a>, une distribution Linux déclarative
avec une gestion de configuration intégrée. Je gère ce petit troupeau avec
<a href="https://colmena.cli.rs/" title="Colmena documentation">Colmena</a>, un outil de déploiement sans état pour NixOS. Ma configuration est
disponible sur <a href="https://github.com/vincentbernat/nixops-take1/" title="vincentbernat/nixops-take1: Playground for NixOps">GitHub</a>.</p>
<h2 id="deployer-des-applications-dans-des-conteneurs">Déployer des applications dans des conteneurs</h2>
<p>Pour une meilleure isolation, chaque application tourne dans un conteneur léger
et éphémère, propulsé par <a href="https://manpages.debian.org/systemd-nspawn.1.html" title="Page de manuel pour systemd-nspawn(1)">systemd-nspawn</a>. Chaque conteneur exécute une
instance allégée de NixOS. Un <a href="https://github.com/vincentbernat/nixops-take1/blob/352cb26a581418d7c50f8377f8dc78016565470c/modules/container.nix" title="nixops-take1/modules/container.nix">module encapsule les options <code>containers</code> de
NixOS</a> pour éviter de répéter les mêmes options pour chaque
application<sup id="fnref:module"><a class="footnote-ref" href="#fn:module">6</a></sup>. Les conteneurs partagent la pile réseau de l'hôte :
l'isolation supplémentaire ne vaut pas la complexité accrue. Pour les alléger,
je désactive aussi quelques services non essentiels.</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> config<span class="p">,</span> lib<span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="k">let</span>
  <span class="ss">cfg</span> <span class="o">=</span> config<span class="o">.</span>luffy<span class="o">.</span>containers<span class="p">;</span>
<span class="k">in</span>
<span class="p">{</span>
  <span class="c1"># Réglages configurables pour notre module</span>
  options<span class="o">.</span>luffy<span class="o">.</span><span class="ss">containers</span> <span class="o">=</span> lib<span class="o">.</span>mkOption <span class="p">{</span>
    <span class="ss">default</span> <span class="o">=</span> <span class="p">{</span> <span class="p">};</span>
    <span class="ss">description</span> <span class="o">=</span> <span class="s2">"Ephemeral containers sharing the host network."</span><span class="p">;</span>
    <span class="ss">type</span> <span class="o">=</span> lib<span class="o">.</span>types<span class="o">.</span>attrsOf <span class="p">(</span>lib<span class="o">.</span>types<span class="o">.</span>submodule <span class="p">{</span>
      <span class="ss">options</span> <span class="o">=</span> <span class="p">{</span>
        <span class="ss">config</span> <span class="o">=</span> lib<span class="o">.</span>mkOption <span class="p">{</span>
          <span class="ss">type</span> <span class="o">=</span> lib<span class="o">.</span>types<span class="o">.</span>deferredModule<span class="p">;</span>
          <span class="ss">default</span> <span class="o">=</span> <span class="p">{</span> <span class="p">};</span>
          <span class="ss">description</span> <span class="o">=</span> <span class="s2">"NixOS configuration of the container."</span><span class="p">;</span>
        <span class="p">};</span>
      <span class="p">};</span>
    <span class="p">});</span>
  <span class="p">};</span>

  <span class="c1"># Traduit nos options en conteneurs NixOS</span>
  <span class="ss">config</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">containers</span> <span class="o">=</span> lib<span class="o">.</span>mapAttrs
      <span class="p">(</span>name<span class="p">:</span> container<span class="p">:</span> <span class="p">{</span>
<span class="hll">        <span class="ss">ephemeral</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span>        <span class="ss">autoStart</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
<span class="hll">        <span class="ss">privateNetwork</span> <span class="o">=</span> <span class="no">false</span><span class="p">;</span>
</span>        <span class="ss">extraFlags</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"--resolv-conf=replace-host"</span> <span class="p">];</span>
        <span class="ss">config</span> <span class="o">=</span> <span class="p">{</span>
<span class="hll">          <span class="ss">imports</span> <span class="o">=</span> <span class="p">[</span> container<span class="o">.</span>config <span class="p">];</span>
</span>          networking<span class="o">.</span>firewall<span class="o">.</span><span class="ss">enable</span> <span class="o">=</span> <span class="no">false</span><span class="p">;</span>
          system<span class="o">.</span><span class="ss">stateVersion</span> <span class="o">=</span> config<span class="o">.</span>system<span class="o">.</span>stateVersion<span class="p">;</span>
          systemd<span class="o">.</span><span class="ss">services</span> <span class="o">=</span> <span class="p">{</span>
<span class="hll">            console-getty<span class="o">.</span><span class="ss">enable</span> <span class="o">=</span> <span class="no">false</span><span class="p">;</span>
</span><span class="hll">            systemd-logind<span class="o">.</span><span class="ss">enable</span> <span class="o">=</span> <span class="no">false</span><span class="p">;</span>
</span><span class="hll">            systemd-oomd<span class="o">.</span><span class="ss">enable</span> <span class="o">=</span> <span class="no">false</span><span class="p">;</span>
</span>          <span class="p">};</span>
        <span class="p">};</span>
      <span class="p">})</span>
      cfg<span class="p">;</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<p>Pour configurer une instance GoatCounter tournant dans un conteneur et écoutant
sur <code>127.0.0.4:8088</code>, nous importons le module<sup id="fnref:import"><a class="footnote-ref" href="#fn:import">7</a></sup> et déclarons le
conteneur dans l'ensemble d'attributs <code>config.luffy.containers</code> :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> pkgs<span class="p">,</span> config<span class="p">,</span> <span class="o">...</span> <span class="p">}:</span> <span class="p">{</span>
  <span class="ss">imports</span> <span class="o">=</span> <span class="p">[</span> <span class="l">./modules/container.nix</span> <span class="p">];</span>
  config<span class="o">.</span>luffy<span class="o">.</span>containers<span class="o">.</span><span class="ss">goatcounter</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">config</span> <span class="o">=</span> <span class="p">{</span>
      services<span class="o">.</span><span class="ss">goatcounter</span> <span class="o">=</span> <span class="p">{</span>
        <span class="ss">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
        <span class="ss">address</span> <span class="o">=</span> <span class="s2">"127.0.0.4"</span><span class="p">;</span>
        <span class="ss">port</span> <span class="o">=</span> <span class="mi">8088</span><span class="p">;</span>
        <span class="ss">proxy</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
      <span class="p">};</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<p>Comme les conteneurs sont éphémères, il faut conserver les données persistantes
dans des répertoires de l'hôte. Nous ajoutons une option <code>mounts</code> et demandons
aux conteneurs de NixOS d'exposer les répertoires ainsi configurés via l'option
<code>bindMounts</code>.</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> config<span class="p">,</span> lib<span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="k">let</span>
  <span class="ss">cfg</span> <span class="o">=</span> config<span class="o">.</span>luffy<span class="o">.</span>containers<span class="p">;</span>
<span class="k">in</span>
<span class="p">{</span>
  options<span class="o">.</span>luffy<span class="o">.</span><span class="ss">containers</span> <span class="o">=</span> lib<span class="o">.</span>mkOption <span class="p">{</span>
    <span class="ss">type</span> <span class="o">=</span> lib<span class="o">.</span>types<span class="o">.</span>attrsOf <span class="p">(</span>lib<span class="o">.</span>types<span class="o">.</span>submodule <span class="p">{</span>
      <span class="ss">options</span> <span class="o">=</span> <span class="p">{</span>
        <span class="ss">mounts</span> <span class="o">=</span> lib<span class="o">.</span>mkOption <span class="p">{</span>
          <span class="ss">type</span> <span class="o">=</span> lib<span class="o">.</span>types<span class="o">.</span>listOf lib<span class="o">.</span>types<span class="o">.</span>str<span class="p">;</span>
          <span class="ss">default</span> <span class="o">=</span> <span class="p">[</span> <span class="p">];</span>
          <span class="ss">description</span> <span class="o">=</span> <span class="s2">"Host directories mounted read-write at the same place."</span><span class="p">;</span>
        <span class="p">};</span>
      <span class="p">};</span>
    <span class="p">});</span>
  <span class="p">};</span>

  <span class="ss">config</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">containers</span> <span class="o">=</span> lib<span class="o">.</span>mapAttrs
      <span class="p">(</span>name<span class="p">:</span> container<span class="p">:</span> <span class="p">{</span>
        <span class="ss">bindMounts</span> <span class="o">=</span>
          lib<span class="o">.</span>genAttrs container<span class="o">.</span>mounts <span class="p">(</span>path<span class="p">:</span> <span class="p">{</span> <span class="ss">hostPath</span> <span class="o">=</span> path<span class="p">;</span> <span class="ss">isReadOnly</span> <span class="o">=</span> <span class="no">false</span><span class="p">;</span> <span class="p">});</span>
      <span class="p">})</span>
      cfg<span class="p">;</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<p>Par exemple, pour conserver la base de données de GoatCounter dans le répertoire
<code>/var/db/goatcounter</code> de l'hôte, nous ajoutons ce répertoire à l'option <code>mounts</code>
et modifions la définition du service pour indiquer à GoatCounter où trouver la
base de données.</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> config<span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="k">let</span>
  <span class="ss">databaseDirectory</span> <span class="o">=</span> <span class="s2">"/var/db/goatcounter"</span><span class="p">;</span>
<span class="k">in</span> <span class="p">{</span>
  config<span class="o">.</span>luffy<span class="o">.</span>containers<span class="o">.</span><span class="ss">goatcounter</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">mounts</span> <span class="o">=</span> <span class="p">[</span> databaseDirectory <span class="p">];</span>
    <span class="ss">config</span> <span class="o">=</span> <span class="p">{</span>
      services<span class="o">.</span><span class="ss">goatcounter</span> <span class="o">=</span> <span class="p">{</span>
        <span class="ss">extraArgs</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"-db=sqlite+</span><span class="si">${</span>databaseDirectory<span class="si">}</span><span class="s2">/db.sqlite"</span> <span class="p">];</span>
      <span class="p">};</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<p>Un conteneur peut aussi avoir besoin de secrets. Colmena peut <a href="https://colmena.cli.rs/unstable/features/keys.html" title="Colmena documentation: Secrets">envoyer des
secrets</a> sans les stocker dans <code>/nix</code>. Nous ajoutons une option
<code>keys</code> à nos conteneurs. Elle prend un ensemble d'attributs associant le nom de
chaque secret à la commande qui le fournit. Ensuite, le module déclare les
secrets requis à Colmena dans l'option <code>deployment.keys</code>, fait dépendre le
conteneur de la présence des secrets, puis les attache au conteneur.</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> config<span class="p">,</span> lib<span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="k">let</span>
  <span class="ss">cfg</span> <span class="o">=</span> config<span class="o">.</span>luffy<span class="o">.</span>containers<span class="p">;</span>
<span class="k">in</span>
<span class="p">{</span>
  options<span class="o">.</span>luffy<span class="o">.</span><span class="ss">containers</span> <span class="o">=</span> lib<span class="o">.</span>mkOption <span class="p">{</span>
    <span class="ss">type</span> <span class="o">=</span> lib<span class="o">.</span>types<span class="o">.</span>attrsOf <span class="p">(</span>lib<span class="o">.</span>types<span class="o">.</span>submodule <span class="p">{</span>
      <span class="ss">options</span> <span class="o">=</span> <span class="p">{</span>
        <span class="ss">keys</span> <span class="o">=</span> lib<span class="o">.</span>mkOption <span class="p">{</span>
          <span class="ss">type</span> <span class="o">=</span> lib<span class="o">.</span>types<span class="o">.</span>attrsOf <span class="p">(</span>lib<span class="o">.</span>types<span class="o">.</span>listOf lib<span class="o">.</span>types<span class="o">.</span>str<span class="p">);</span>
          <span class="ss">default</span> <span class="o">=</span> <span class="p">{</span> <span class="p">};</span>
          <span class="ss">description</span> <span class="o">=</span> <span class="s2">"Secrets, as a command to run locally. They are mounted in /etc."</span><span class="p">;</span>
        <span class="p">};</span>
      <span class="p">};</span>
    <span class="p">});</span>
  <span class="p">};</span>

  <span class="ss">config</span> <span class="o">=</span> <span class="p">{</span>
    <span class="c1"># Colmena place chaque secret dans `/var/keys` et le rend accessible</span>
    <span class="c1"># au groupe "keys".</span>
    deployment<span class="o">.</span><span class="ss">keys</span> <span class="o">=</span> lib<span class="o">.</span>concatMapAttrs
      <span class="p">(</span>_<span class="p">:</span> container<span class="p">:</span> lib<span class="o">.</span>mapAttrs
        <span class="p">(</span>_<span class="p">:</span> keyCommand<span class="p">:</span> <span class="p">{</span>
          <span class="k">inherit</span> keyCommand<span class="p">;</span>
          <span class="ss">group</span> <span class="o">=</span> <span class="s2">"keys"</span><span class="p">;</span>
          <span class="ss">permissions</span> <span class="o">=</span> <span class="s2">"0640"</span><span class="p">;</span>
          <span class="ss">destDir</span> <span class="o">=</span> <span class="s2">"/var/keys"</span><span class="p">;</span>
        <span class="p">})</span>
        container<span class="o">.</span>keys<span class="p">)</span>
      cfg<span class="p">;</span>

    <span class="c1"># Le conteneur ne peut démarrer que si les secrets requis sont disponibles.</span>
    systemd<span class="o">.</span><span class="ss">services</span> <span class="o">=</span> lib<span class="o">.</span>mapAttrs'
      <span class="p">(</span>name<span class="p">:</span> container<span class="p">:</span>
        <span class="k">let</span>
          <span class="ss">units</span> <span class="o">=</span> <span class="nb">map</span> <span class="p">(</span>key<span class="p">:</span> <span class="s2">"</span><span class="si">${</span>key<span class="si">}</span><span class="s2">-key.service"</span><span class="p">)</span> <span class="p">(</span>lib<span class="o">.</span>attrNames container<span class="o">.</span>keys<span class="p">);</span>
        <span class="k">in</span>
        lib<span class="o">.</span>nameValuePair <span class="s2">"container@</span><span class="si">${</span>name<span class="si">}</span><span class="s2">"</span> <span class="p">{</span>
          <span class="ss">requires</span> <span class="o">=</span> units<span class="p">;</span>
          <span class="ss">after</span> <span class="o">=</span> units<span class="p">;</span>
        <span class="p">})</span>
      cfg<span class="p">;</span>

    <span class="c1"># Attache chaque secret dans le conteneur.</span>
    <span class="ss">containers</span> <span class="o">=</span> lib<span class="o">.</span>mapAttrs
      <span class="p">(</span>name<span class="p">:</span> container<span class="p">:</span> <span class="p">{</span>
        <span class="ss">bindMounts</span> <span class="o">=</span> lib<span class="o">.</span>mapAttrs'
          <span class="p">(</span>key<span class="p">:</span> _<span class="p">:</span> lib<span class="o">.</span>nameValuePair <span class="s2">"/etc/</span><span class="si">${</span>key<span class="si">}</span><span class="s2">"</span> <span class="p">{</span>
            <span class="ss">hostPath</span> <span class="o">=</span> <span class="s2">"/var/keys/</span><span class="si">${</span>key<span class="si">}</span><span class="s2">"</span><span class="p">;</span>
            <span class="ss">isReadOnly</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
          <span class="p">})</span>
          container<span class="o">.</span>keys<span class="p">;</span>
      <span class="p">})</span>
      cfg<span class="p">;</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<p>Par exemple, GoatCounter a besoin d'identifiants pour télécharger la base de
données GeoIP. Je fournis une commande locale pour récupérer le secret depuis
mon gestionnaire de mots de passe et je l'expose dans le conteneur via le
fichier d'environnement <code>/etc/goatcounter.env</code>.</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> pkgs<span class="p">,</span> config<span class="p">,</span> <span class="o">...</span> <span class="p">}:</span> 
<span class="k">let</span>
  <span class="ss">keyCommand</span> <span class="o">=</span> variable<span class="p">:</span> <span class="p">[</span>
    <span class="s2">"</span><span class="si">${</span>pkgs<span class="o">.</span>runtimeShell<span class="si">}</span><span class="s2">"</span>
    <span class="s2">"-c"</span>
    <span class="s2">"pass show personal/nixops/secrets | grep '^</span><span class="si">${</span>variable<span class="si">}</span><span class="s2">='"</span>
  <span class="p">];</span>
<span class="k">in</span> <span class="p">{</span>
  config<span class="o">.</span>luffy<span class="o">.</span>containers<span class="o">.</span><span class="ss">goatcounter</span> <span class="o">=</span> <span class="p">{</span>
<span class="hll">    keys<span class="o">.</span><span class="s2">"goatcounter.env"</span> <span class="o">=</span> keyCommand <span class="s2">"GOATCOUNTER_GEODB"</span><span class="p">;</span>
</span>    <span class="ss">config</span> <span class="o">=</span> <span class="p">{</span>
      systemd<span class="o">.</span>services<span class="o">.</span>goatcounter<span class="o">.</span><span class="ss">serviceConfig</span> <span class="o">=</span> <span class="p">{</span>
        <span class="ss">EnvironmentFile</span> <span class="o">=</span> <span class="s2">"/etc/goatcounter.env"</span><span class="p">;</span>
        <span class="ss">SupplementaryGroups</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"keys"</span> <span class="p">];</span>
      <span class="p">};</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<h2 id="serveur-goatcounter">Serveur GoatCounter</h2>
<p>Nixpkgs fournit déjà un paquet pour GoatCounter. En surchargeant les attributs
<code>src</code> et <code>vendorHash</code>, je réutilise sa définition pour ma version personnalisée
avec le proxy :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> goatcounter<span class="p">,</span> fetchFromGitHub <span class="p">}:</span>
goatcounter<span class="o">.</span>overrideAttrs <span class="p">(</span>_<span class="p">:</span> <span class="p">{</span>
  <span class="ss">src</span> <span class="o">=</span> fetchFromGitHub <span class="p">{</span>
    <span class="ss">owner</span> <span class="o">=</span> <span class="s2">"vincentbernat"</span><span class="p">;</span>
    <span class="ss">repo</span> <span class="o">=</span> <span class="s2">"goatcounter"</span><span class="p">;</span>
    <span class="ss">rev</span> <span class="o">=</span> <span class="s2">"feature/proxy"</span><span class="p">;</span>
    <span class="ss">hash</span> <span class="o">=</span> <span class="s2">"sha256-dJRlQlFu3tjcEgabT1LEbyFrasJlhmYu4L/T7EkoNcY="</span><span class="p">;</span>
  <span class="p">};</span>
  <span class="ss">vendorHash</span> <span class="o">=</span> <span class="s2">"sha256-c9Q5OrbZR+q6pD3SgPPWe8JUzcZco1AVUKGaV61k5DE="</span><span class="p">;</span>
<span class="p">})</span>
</code></pre></div>

<p>J'ai écrit un <a href="https://github.com/vincentbernat/nixops-take1/blob/352cb26a581418d7c50f8377f8dc78016565470c/modules/goatcounter.nix" title="nixops-take1/modules/goatcounter.nix">module NixOS</a> pour encapsuler GoatCounter : la
définition du conteneur, celle du service et les secrets. Le module accepte les
options suivantes : <code>package</code>, <code>serve.enable</code>, <code>serve.listenAddress</code>,
<code>serve.port</code> et <code>serve.databaseFile</code>. J'ai déjà détaillé la configuration du
conteneur dans la section précédente. Finalement, j'ai choisi de ne pas
réutiliser le module GoatCounter de NixOS : il est plutôt simple et le réécrire
permet de me protéger de changements futurs inattendus.</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> config<span class="p">,</span> pkgs<span class="p">,</span> lib<span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="k">let</span>
  <span class="ss">cfg</span> <span class="o">=</span> config<span class="o">.</span>luffy<span class="o">.</span>goatcounter<span class="p">;</span>
  <span class="ss">databaseDirectory</span> <span class="o">=</span> <span class="nb">builtins</span><span class="o">.</span><span class="nb">dirOf</span> cfg<span class="o">.</span>serve<span class="o">.</span>databaseFile<span class="p">;</span>
  <span class="ss">chown</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span>pkgs<span class="o">.</span>coreutils<span class="si">}</span><span class="s2">/bin/chown -R"</span><span class="p">;</span>
<span class="k">in</span> <span class="p">{</span>
  config<span class="o">.</span>luffy<span class="o">.</span>containers<span class="o">.</span><span class="ss">goatcounter</span> <span class="o">=</span> <span class="p">{</span>
    config<span class="o">.</span>systemd<span class="o">.</span>services<span class="o">.</span><span class="ss">goatcounter</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">description</span> <span class="o">=</span> <span class="s2">"GoatCounter Web Analytics"</span><span class="p">;</span>
      <span class="ss">wantedBy</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"multi-user.target"</span> <span class="p">];</span>
      <span class="ss">serviceConfig</span> <span class="o">=</span> <span class="p">{</span>
        <span class="ss">EnvironmentFile</span> <span class="o">=</span> <span class="s2">"/etc/goatcounter.env"</span><span class="p">;</span>
        <span class="ss">SupplementaryGroups</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"keys"</span> <span class="p">];</span>
        <span class="ss">DynamicUser</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
        <span class="ss">Restart</span> <span class="o">=</span> <span class="s2">"always"</span><span class="p">;</span>
        <span class="ss">ExecStart</span> <span class="o">=</span> lib<span class="o">.</span>escapeShellArgs <span class="p">[</span>
          <span class="p">(</span>lib<span class="o">.</span>getExe cfg<span class="o">.</span>package<span class="p">)</span>
          <span class="s2">"serve"</span>
          <span class="s2">"-listen=</span><span class="si">${</span>cfg<span class="o">.</span>serve<span class="o">.</span>listenAddress<span class="si">}</span><span class="s2">:</span><span class="si">${</span><span class="nb">toString</span> cfg<span class="o">.</span>serve<span class="o">.</span>port<span class="si">}</span><span class="s2">"</span>
          <span class="s2">"-tls=none"</span>
          <span class="s2">"-db=sqlite+</span><span class="si">${</span>cfg<span class="o">.</span>serve<span class="o">.</span>databaseFile<span class="si">}</span><span class="s2">"</span>
          <span class="s2">"-automigrate"</span>
        <span class="p">];</span>
        <span class="c1"># Donne la base de données à l'utilisateur dynamique "goatcounter".</span>
        <span class="ss">ExecStartPre</span> <span class="o">=</span> <span class="s2">"+</span><span class="si">${</span>chown<span class="si">}</span><span class="s2"> goatcounter:goatcounter </span><span class="si">${</span>databaseDirectory<span class="si">}</span><span class="s2">"</span><span class="p">;</span>
        <span class="ss">ReadWritePaths</span> <span class="o">=</span> databaseDirectory<span class="p">;</span>
      <span class="p">};</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<p>Le bout de code suivant configure GoatCounter pour écouter sur
<code>127.0.0.4:8088</code> :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span>
  luffy<span class="o">.</span><span class="ss">goatcounter</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">serve</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
      <span class="ss">listenAddress</span> <span class="o">=</span> <span class="s2">"127.0.0.4"</span><span class="p">;</span>
      <span class="ss">port</span> <span class="o">=</span> <span class="mi">8088</span><span class="p">;</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<p>La dernière étape consiste à configurer nginx pour exposer GoatCounter sur
Internet. Je désactive le point de terminaison <code>/count</code>, car sa fonction est
assurée par le proxy local.</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> config<span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="k">let</span>
  <span class="ss">cfg</span> <span class="o">=</span> config<span class="o">.</span>luffy<span class="o">.</span>goatcounter<span class="o">.</span>serve<span class="p">;</span>
<span class="k">in</span>
<span class="p">{</span>
  services<span class="o">.</span>nginx<span class="o">.</span>virtualHosts<span class="o">.</span><span class="s2">"goatcounter.luffy.cx"</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">forceSSL</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
    <span class="ss">locations</span> <span class="o">=</span> <span class="p">{</span>
      <span class="s2">"/"</span> <span class="o">=</span> <span class="p">{</span>
        <span class="ss">proxyPass</span> <span class="o">=</span> <span class="s2">"http://</span><span class="si">${</span>cfg<span class="o">.</span>listenAddress<span class="si">}</span><span class="s2">:</span><span class="si">${</span><span class="nb">toString</span> cfg<span class="o">.</span>port<span class="si">}</span><span class="s2">"</span><span class="p">;</span>
      <span class="p">};</span>
      <span class="s2">"= /count"</span><span class="o">.</span><span class="ss">extraConfig</span> <span class="o">=</span> <span class="s s-Multiline">''</span>
<span class="s s-Multiline">        return 404;</span>
<span class="s s-Multiline">      ''</span><span class="p">;</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<h2 id="proxy-goatcounter">Proxy GoatCounter</h2>
<p>Le même <a href="https://github.com/vincentbernat/nixops-take1/blob/352cb26a581418d7c50f8377f8dc78016565470c/modules/goatcounter.nix" title="nixops-take1/modules/goatcounter.nix">module NixOS</a> configure le proxy local, avec les
options suivantes : <code>proxy.enable</code>, <code>proxy.listenAddress</code>, <code>proxy.port</code> et
<code>proxy.site</code> (le site qui reçoit les lots de pages vues). Le proxy local n'a
aucune donnée persistante, mais il a besoin de la clé d'API pour s'authentifier
auprès de l'instance principale de GoatCounter : son conteneur utilise l'option
<code>keys</code>, mais pas l'option <code>mounts</code>.</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> config<span class="p">,</span> pkgs<span class="p">,</span> lib<span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="k">let</span>
  <span class="ss">cfg</span> <span class="o">=</span> config<span class="o">.</span>luffy<span class="o">.</span>goatcounter<span class="p">;</span>
  <span class="ss">keyCommand</span> <span class="o">=</span> _<span class="p">:</span> <span class="p">[</span> <span class="s2">"…"</span> <span class="p">];</span>
<span class="k">in</span>
<span class="p">{</span>
  config<span class="o">.</span>luffy<span class="o">.</span>containers<span class="o">.</span><span class="ss">goatcounter-proxy</span> <span class="o">=</span> <span class="p">{</span>
<span class="hll">    keys<span class="o">.</span><span class="s2">"goatcounter-proxy.env"</span> <span class="o">=</span> keyCommand <span class="s2">"GOATCOUNTER_API_KEY"</span><span class="p">;</span>
</span>    config<span class="o">.</span>systemd<span class="o">.</span>services<span class="o">.</span><span class="ss">goatcounter</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">description</span> <span class="o">=</span> <span class="s2">"GoatCounter Proxy."</span><span class="p">;</span>
      <span class="ss">wantedBy</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"multi-user.target"</span> <span class="p">];</span>
      <span class="ss">serviceConfig</span> <span class="o">=</span> <span class="p">{</span>
        <span class="ss">EnvironmentFile</span> <span class="o">=</span> <span class="s2">"/etc/goatcounter-proxy.env"</span><span class="p">;</span>
        <span class="ss">SupplementaryGroups</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"keys"</span> <span class="p">];</span>
        <span class="ss">DynamicUser</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
        <span class="ss">Restart</span> <span class="o">=</span> <span class="s2">"always"</span><span class="p">;</span>
        <span class="ss">ExecStart</span> <span class="o">=</span> lib<span class="o">.</span>escapeShellArgs <span class="p">[</span>
          <span class="p">(</span>lib<span class="o">.</span>getExe cfg<span class="o">.</span>package<span class="p">)</span>
          <span class="s2">"proxy"</span>
          <span class="s2">"-site=</span><span class="si">${</span>cfg<span class="o">.</span>proxy<span class="o">.</span>site<span class="si">}</span><span class="s2">"</span>
          <span class="s2">"-listen=</span><span class="si">${</span>cfg<span class="o">.</span>proxy<span class="o">.</span>listenAddress<span class="si">}</span><span class="s2">:</span><span class="si">${</span><span class="nb">toString</span> cfg<span class="o">.</span>proxy<span class="o">.</span>port<span class="si">}</span><span class="s2">"</span>
          <span class="s2">"-ratelimit=10/1"</span>  <span class="c1"># 10 requêtes par seconde par IP</span>
        <span class="p">];</span>
      <span class="p">};</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<p>Sur chaque serveur, j'active le proxy local avec le bout de code suivant. La
configuration nginx présentée plus haut expose le point de terminaison <code>/count</code>
sous le même domaine que mon blog.</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span>
  luffy<span class="o">.</span><span class="ss">goatcounter</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">proxy</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
      <span class="ss">site</span> <span class="o">=</span> <span class="s2">"goatcounter.luffy.cx"</span><span class="p">;</span>
      <span class="ss">listenAddress</span> <span class="o">=</span> <span class="s2">"127.0.0.3"</span><span class="p">;</span>
      <span class="ss">port</span> <span class="o">=</span> <span class="mi">8087</span><span class="p">;</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<h2 id="sauvegarde-de-la-base-de-donnees-sqlite-avec-litestream">Sauvegarde de la base de données SQLite avec Litestream</h2>
<p><a href="https://litestream.io/" title="Litestream: Streaming SQLite Replication">Litestream</a> est un outil de réplication en continu pour les bases de données
SQLite. Il compresse les changements placés dans le journal d'écriture
anticipée (<abbr title="Write-ahead log">WAL</abbr>) situé à côté de la base de données et les envoie vers une
destination distante. J'encapsule sa configuration dans un <a href="https://github.com/vincentbernat/nixops-take1/blob/352cb26a581418d7c50f8377f8dc78016565470c/modules/litestream.nix" title="nixops-take1/modules/litestream.nix">module
NixOS</a>, qui prend un ensemble d'attributs <code>databases</code> associant
un nom au chemin de la base de données à sauvegarder.</p>
<p>Litestream tourne lui aussi dans un conteneur. Je monte les bases de données à
répliquer, ainsi que les secrets pour pousser les sauvegardes vers une <a href="https://www.hetzner.com/storage/storage-box/" title="Hetzner Storage Box"><em>storage
box</em> de Hetzner</a> via <abbr title="Secure File Transfer Protocol">SFTP</abbr> :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> config<span class="p">,</span> pkgs<span class="p">,</span> lib<span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="k">let</span>
  <span class="ss">cfg</span> <span class="o">=</span> config<span class="o">.</span>luffy<span class="o">.</span>litestream<span class="p">;</span>
  <span class="ss">databaseDirs</span> <span class="o">=</span> lib<span class="o">.</span>unique <span class="p">(</span><span class="nb">map</span> <span class="nb">builtins</span><span class="o">.</span><span class="nb">dirOf</span> <span class="p">(</span><span class="nb">builtins</span><span class="o">.</span>attrValues cfg<span class="o">.</span>databases<span class="p">));</span>
<span class="k">in</span>
<span class="p">{</span>
  <span class="ss">config</span> <span class="o">=</span> lib<span class="o">.</span>mkIf <span class="p">(</span>cfg<span class="o">.</span>databases <span class="o">!=</span> <span class="p">{</span> <span class="p">})</span> <span class="p">{</span>
    luffy<span class="o">.</span>containers<span class="o">.</span><span class="ss">litestream</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">mounts</span> <span class="o">=</span> databaseDirs<span class="p">;</span>
      keys<span class="o">.</span><span class="s2">"litestream.env"</span> <span class="o">=</span> <span class="p">[</span>
        <span class="s2">"</span><span class="si">${</span>pkgs<span class="o">.</span>runtimeShell<span class="si">}</span><span class="s2">"</span>
        <span class="s2">"-c"</span>
        <span class="s2">"pass show personal/nixops/secrets | grep '^SQLITE_BACKUP_'"</span>
      <span class="p">];</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<p>Dans le conteneur, je configure Litestream à travers les options
<code>services.litestream</code> de NixOS :</p>
<ul>
<li>des instantanés complets chaque jour, conservés 15 jours ;</li>
<li>trois <a href="https://fly.io/blog/litestream-v050-is-here/" title="Litestream v0.5.0 is Here">niveaux de compactage</a> pour les transactions :
  5 minutes, 30 minutes et 3 heures ;</li>
<li>la reprise automatique<sup id="fnref:auto-recover"><a class="footnote-ref" href="#fn:auto-recover">8</a></sup> ;</li>
<li>un réplica stocké dans un répertoire portant le nom de l'hôte ;</li>
<li>des identifiants lus depuis <code>/etc/litestream.env</code> et exposés par expansion de variables.</li>
</ul>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> config<span class="p">,</span> pkgs<span class="p">,</span> lib<span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="k">let</span>
  <span class="ss">cfg</span> <span class="o">=</span> config<span class="o">.</span>luffy<span class="o">.</span>litestream<span class="p">;</span>
<span class="k">in</span>
<span class="p">{</span>
  config<span class="o">.</span>luffy<span class="o">.</span>containers<span class="o">.</span><span class="ss">litestream</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">config</span> <span class="o">=</span> <span class="p">{</span>
      <span class="c1"># Les bases de données appartiennent à des utilisateurs dynamiques, dont</span>
      <span class="c1"># l'UID n'est pas connu ici : Litestream tourne donc en root.</span>
      systemd<span class="o">.</span>services<span class="o">.</span>litestream<span class="o">.</span><span class="ss">serviceConfig</span> <span class="o">=</span> <span class="p">{</span>
        <span class="ss">User</span> <span class="o">=</span> lib<span class="o">.</span>mkForce <span class="s2">"root"</span><span class="p">;</span>
        <span class="ss">Group</span> <span class="o">=</span> lib<span class="o">.</span>mkForce <span class="s2">"root"</span><span class="p">;</span>
      <span class="p">};</span>
      <span class="c1"># Utilise le service NixOS.</span>
      services<span class="o">.</span><span class="ss">litestream</span> <span class="o">=</span> <span class="p">{</span>
        <span class="ss">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
        <span class="ss">environmentFile</span> <span class="o">=</span> <span class="s2">"/etc/litestream.env"</span><span class="p">;</span>
        <span class="ss">settings</span> <span class="o">=</span> <span class="p">{</span>
          <span class="ss">auto-recover</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
          <span class="ss">snapshot</span> <span class="o">=</span> <span class="p">{</span>
            <span class="ss">interval</span> <span class="o">=</span> <span class="s2">"24h"</span><span class="p">;</span>
            <span class="ss">retention</span> <span class="o">=</span> <span class="s2">"360h"</span><span class="p">;</span>
          <span class="p">};</span>
          <span class="ss">levels</span> <span class="o">=</span> <span class="p">[</span>
            <span class="p">{</span> <span class="ss">interval</span> <span class="o">=</span> <span class="s2">"5m"</span><span class="p">;</span> <span class="p">}</span>
            <span class="p">{</span> <span class="ss">interval</span> <span class="o">=</span> <span class="s2">"30m"</span><span class="p">;</span> <span class="p">}</span>
            <span class="p">{</span> <span class="ss">interval</span> <span class="o">=</span> <span class="s2">"3h"</span><span class="p">;</span> <span class="p">}</span>
          <span class="p">];</span>
          <span class="ss">dbs</span> <span class="o">=</span> lib<span class="o">.</span>mapAttrsToList
            <span class="p">(</span>name<span class="p">:</span> path<span class="p">:</span> <span class="p">{</span>
              <span class="k">inherit</span> path<span class="p">;</span>
              <span class="ss">replica</span> <span class="o">=</span> <span class="p">{</span>
                <span class="ss">type</span> <span class="o">=</span> <span class="s2">"sftp"</span><span class="p">;</span>
                <span class="ss">host</span> <span class="o">=</span> <span class="s2">"</span><span class="se">\$</span><span class="s2">{SQLITE_BACKUP_HOST}"</span><span class="p">;</span>
                <span class="ss">user</span> <span class="o">=</span> <span class="s2">"</span><span class="se">\$</span><span class="s2">{SQLITE_BACKUP_USER}"</span><span class="p">;</span>
                <span class="ss">password</span> <span class="o">=</span> <span class="s2">"</span><span class="se">\$</span><span class="s2">{SQLITE_BACKUP_PASSWORD}"</span><span class="p">;</span>
                <span class="ss">host-key</span> <span class="o">=</span> <span class="s2">"</span><span class="se">\$</span><span class="s2">{SQLITE_BACKUP_HOSTKEY}"</span><span class="p">;</span>
                <span class="ss">path</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span>config<span class="o">.</span>networking<span class="o">.</span>hostName<span class="si">}</span><span class="s2">/</span><span class="si">${</span>name<span class="si">}</span><span class="s2">"</span><span class="p">;</span>
              <span class="p">};</span>
            <span class="p">})</span>
            cfg<span class="o">.</span>databases<span class="p">;</span>
        <span class="p">};</span>
      <span class="p">};</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<p>Pour sauvegarder la base de données de GoatCounter, je déclare un attribut
<code>goatcounter</code> dans <code>luffy.litestream.databases</code> avec pour valeur le chemin de la
base de données :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> config<span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="k">let</span>
  <span class="ss">cfg</span> <span class="o">=</span> config<span class="o">.</span>luffy<span class="o">.</span>goatcounter<span class="o">.</span>serve<span class="p">;</span>
<span class="k">in</span>
<span class="p">{</span>
  luffy<span class="o">.</span>litestream<span class="o">.</span>databases<span class="o">.</span><span class="ss">goatcounter</span> <span class="o">=</span> cfg<span class="o">.</span>databaseFile<span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<p>Sur le serveur <abbr title="Secure File Transfer Protocol">SFTP</abbr>, nous pouvons inspecter le travail de Litestream, avec les
transactions compactées et les instantanés complets :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">❯ </span>ls<span class="w"> </span>web02/goatcounter/ltx
<span class="go">web02/goatcounter/ltx/0</span>
<span class="go">web02/goatcounter/ltx/1</span>
<span class="go">web02/goatcounter/ltx/2</span>
<span class="go">web02/goatcounter/ltx/3</span>
<span class="go">web02/goatcounter/ltx/9</span>
<span class="gp">❯ </span>ls<span class="w"> </span>-lh<span class="w"> </span>web02/goatcounter/ltx/1
<span class="go">29.1K Sep  5 01:25 0000000000003f2a-0000000000003f2b.ltx</span>
<span class="go">72.4K Sep  5 02:03 0000000000003f2c-0000000000003f2d.ltx</span>
<span class="go">63.3K Sep  5 02:24 0000000000003f2e-0000000000003f2f.ltx</span>
<span class="go">[…]</span>
<span class="gp">❯ </span>ls<span class="w"> </span>-lh<span class="w"> </span>web02/goatcounter/ltx/9
<span class="go"> 8.5M Sep  5 02:00 0000000000000001-0000000000003f2b.ltx</span>
<span class="go"> 8.5M Sep  6 02:03 0000000000000001-0000000000004008.ltx</span>
<span class="go"> 8.6M Sep  7 02:03 0000000000000001-00000000000043a8.ltx</span>
<span class="go">[…]</span>
</code></pre></div>

<p>Nous pouvons restaurer la base de données depuis la sauvegarde avec quelques
commandes shell. D'abord, nous arrêtons les conteneurs. Ensuite, nous mettons de
côté la base de données endommagée, invoquons <code>litestream restore</code> depuis le bon
environnement et redémarrons les conteneurs<sup id="fnref:slow"><a class="footnote-ref" href="#fn:slow">9</a></sup>.</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp"># </span>systemctl<span class="w"> </span>stop<span class="w"> </span>container@goatcounter<span class="w"> </span>container@litestream
<span class="gp"># </span>mv<span class="w"> </span>/var/db/goatcounter/db.sqlite<span class="o">{</span>,.old<span class="o">}</span>
<span class="gp"># </span><span class="o">(</span><span class="w"> </span>.<span class="w"> </span>/etc/nixos-containers/litestream.conf<span class="w"> </span><span class="p">;</span><span class="w"> </span>
<span class="gp">&gt; </span><span class="w">  </span><span class="nb">set</span><span class="w"> </span>-a<span class="w"> </span><span class="p">;</span><span class="w"> </span>.<span class="w"> </span>/var/keys/litestream.env<span class="w"> </span><span class="p">;</span><span class="w"> </span><span class="nb">set</span><span class="w"> </span>+a<span class="w"> </span><span class="p">;</span>
<span class="gp">&gt; </span><span class="w">  </span><span class="nv">$SYSTEM_PATH</span>/sw/bin/litestream<span class="w"> </span><span class="se">\</span>
<span class="gp">&gt; </span><span class="w">    </span>restore<span class="w"> </span>-config<span class="w"> </span><span class="nv">$SYSTEM_PATH</span>/etc/litestream.yml<span class="w"> </span>/var/db/goatcounter/db.sqlite<span class="o">)</span>
<span class="gp"># </span>ls<span class="w"> </span>-lh<span class="w"> </span>/var/db/goatcounter/db.sqlite
<span class="go">-rw-r--r-- 1 root root 20M Sep 20 07:33 /var/db/goatcounter/db.sqlite</span>
<span class="gp"># </span>systemctl<span class="w"> </span>start<span class="w"> </span>container@goatcounter<span class="w"> </span>container@litestream
</code></pre></div>

<hr/>
<p>Dix ans après <a href="/fr/blog/2018-blog-respect-vie-privee" title="Un blog plus respectueux de la vie privée">avoir retiré Google Analytics</a>, les
statistiques en JavaScript sont de retour sur ce blog, mais sans stocker de
cookies ni d'adresses IP, et sans se reposer sur un tiers. J'écris d'abord pour
moi, notamment parce que cela me permet de creuser un sujet et de m'y référer
des années plus tard. Cependant, en savoir un peu plus sur mes lecteurs humains
est un bonus appréciable, y compris sur ceux qui n'activent pas JavaScript. 🐐</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:scrub">
<p>Nginx brouille les adresses IP avant de les enregistrer, grâce au <a href="https://github.com/masonicboom/ipscrub" title="ipscrub: IP address anonymizer module for nginx">module
<code>ipscrub</code></a>. <a class="footnote-backref" href="#fnref:scrub" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:bot-filter">
<p>GoatCounter filtre déjà certains robots grâce à leur agent utilisateur ou à
leur adresse IP. Mais les robots pilleurs mentent sur leur agent utilisateur
et se cachent derrière des <a href="https://jacob.gold/posts/residential-proxies-are-a-national-security-threat/" title="Residential Proxies Are a National Security Threat">proxys résidentiels</a>. <a class="footnote-backref" href="#fnref:bot-filter" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:referrer">
<p>Sans JavaScript, impossible d'envoyer la véritable origine. J'indique
« NoJS » à la place. <a class="footnote-backref" href="#fnref:referrer" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:rss">
<p>Il me manque les humains lisant le flux RSS. Je ne suis pas à l'aise avec
l'idée d'ajouter un <a href="https://www.goatcounter.com/help/pixel" title="GoatCounter documentation: tracking pixel without JavaScript">pixel transparent</a>, et les robots le
chargeraient sans doute, ce qui fausserait les statistiques. Je me passe de
compter ces lecteurs. <a class="footnote-backref" href="#fnref:rss" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:css-test">
<p>En février 2025, j'ai testé une autre approche reposant uniquement sur CSS
sans détecter d'interaction : chaque page chargeait une image vide comme
arrière-plan. Sur un mois, elle a enregistré jusqu'à dix fois moins de pages
vues que les journaux de nginx, même après avoir retiré les robots qui
s'identifiaient comme tels. Par rapport à cette expérience, 18 mois plus
tard, GoatCounter compte environ trois fois moins de vues. Comme les
périodes diffèrent, la comparaison n'est pas rigoureuse, mais elle donne une
idée de l'efficacité de cette preuve d'humanité. <a class="footnote-backref" href="#fnref:css-test" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:module">
<p>Un <a href="https://nixos.org/manual/nixos/stable/#sec-writing-modules" title="NixOS Manual: Writing NixOS Modules">module NixOS</a> est une fonction qui reçoit la configuration
de l'ensemble du système dans <code>config</code> et renvoie trois attributs :</p>
<ul>
<li><code>imports</code> liste d'autres modules à importer ;</li>
<li><code>options</code> déclare les réglages configurables par l'utilisateur ;</li>
<li><code>config</code> attribue des valeurs aux options déclarées par n'importe quel module, comme <code>containers</code> de NixOS.</li>
</ul>
<p>Si un module ne déclare aucune option, il est possible de renvoyer directement les attributs
contenus dans <code>config</code>. Si un module n'a besoin d'aucun argument, vous pouvez le
définir comme un simple ensemble d'attributs. <a class="footnote-backref" href="#fnref:module" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:import">
<p>La plupart du temps, un import explicite est inutile. NixOS <a href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/module-list.nix" title="nixpkgs/nixos/modules/module-list.nix">importe
automatiquement</a> les modules livrés avec Nixpkgs. Pour mes
propres modules, <a href="https://github.com/vincentbernat/nixops-take1/blob/master/modules/default.nix" title="nixops-take1/modules/default.nix">un peu de code</a> les importe aussi
automatiquement. <a class="footnote-backref" href="#fnref:import" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:auto-recover">
<p>Litestream <a href="https://litestream.io/reference/config/#auto-recover" title="Litestream: auto-recover option">déconseille l'option <code>auto-recover</code></a>, car elle
peut entraîner une perte de données. Mais je ne surveille pas assez
attentivement mes serveurs et je préfère des sauvegardes toujours
fonctionnelles à un faible risque de perdre les derniers enregistrements. <a class="footnote-backref" href="#fnref:auto-recover" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:slow">
<p>Chez moi, le processus est lent : environ 20 minutes pour une base de
données de 20 Mio. Vous pouvez tester en restaurant vers une copie avec
l'option <code>-o</code>, mais il faut tout de même arrêter le conteneur Litestream. <a class="footnote-backref" href="#fnref:slow" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">PKI hors ligne avec 3 YubiKeys et une carte ARM</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2025-pki-hors-ligne-yubikeys" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2025-pki-hors-ligne-yubikeys#isso-thread" rel="replies" type="text/html"/>
    <updated>2025-03-17T07:12:54Z</updated>
    <id>http://www.luffy.cx/fr/blog/2025-pki-hors-ligne-yubikeys.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>Une <abbr title="Public Key Infrastructure">PKI</abbr> hors ligne renforce la sécurité en isolant physiquement l'autorité de
certification des menaces réseau. Une <a href="https://www.yubico.com/products/yubikey-5-overview/" title="Discover YubiKey 5">YubiKey</a> est une solution économique
pour stocker un certificat racine. Vous avez également besoin d'un environnement
isolé pour les opérations sur l'autorité de certification racine.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 400px"><span class="lf-media-inner" style="padding-bottom: 86.500%"><img alt="PKI s'appuyant sur un ensemble de 3 YubiKeys : 2 pour l'autorité de&#10;certification racine et 1 pour l'autorité de certification&#10;intermédiaire." src="https://media-bernat-ch.b-cdn.net/images/3-yubikeys@1x.8beb69c4aaa2a2.jpg" srcset="https://media-bernat-ch.b-cdn.net/images/3-yubikeys@1x.8beb69c4aaa2a2.jpg 503w,https://media-bernat-ch.b-cdn.net/images/3-yubikeys@2x.4c6fa0e083e9cc.jpg 800w" sizes="auto, (max-width: 400px) 100vw, 400px" width="400" height="346" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAK1AQMAAAAKYhhvAAAAA1BMVEW0o5wZvltTAAAAW0lEQVR42u3BMQEAAADCoPVPbQlPoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAmwEReAABFX7yMQAAAABJRU5ErkJggg==)"/></span></div><figcaption>PKI hors ligne sauvegardée par 3 YubiKeys</figcaption></figure>
<p>Ce billet décrit un système <abbr title="Public Key Infrastructure">PKI</abbr> hors ligne utilisant les composants suivants :</p>
<ul>
<li>2 YubiKeys pour l'<strong>autorité de certification racine</strong> (avec une validité de
  20 ans) ;</li>
<li>1 YubiKey pour l'<strong>autorité de certification intermédiaire</strong> (avec une
  validité de 5 ans) ;</li>
<li>1 carte <a href="https://libre.computer/products/aml-s905x-cc-v2/" title="Sweet Potato AML-S905X-CC-V2">Libre Computer Sweet Potato</a> comme <strong>nano-ordinateur isolé</strong>.</li>
</ul>
<p>Si nécessaire, il est possible d'ajouter plus de YubiKeys comme sauvegarde de
l'autorité de certification racine. Ce n'est pas utile pour l'autorité de
certification intermédiaire car vous pouvez en générer une nouvelle si
l'actuelle est détruite.</p>
<div class="toc">
<ul>
<li><a href="#la-partie-logicielle">La partie logicielle</a></li>
<li><a href="#la-partie-materielle">La partie matérielle</a></li>
<li><a href="#lassemblage-nix">L'assemblage Nix</a></li>
</ul>
</div>
<h1 id="la-partie-logicielle">La partie logicielle</h1>
<p><a href="https://github.com/vincentbernat/offline-pki" title="Offline PKI system"><code>offline-pki</code></a> est une petite application Python pour gérer une <abbr title="Public Key Infrastructure">PKI</abbr>
hors ligne. Elle s'appuie sur <a href="https://github.com/Yubico/yubikey-manager" title="Python library for configuring any YubiKey">yubikey-manager</a> pour gérer les YubiKeys et sur
<a href="https://cryptography.io/en/latest/" title="cryptography's documentation">cryptography</a> pour les opérations cryptographiques non exécutées par les
YubiKeys. L'application a certains choix de conception opiniâtres. Notamment, la
cryptographie est codée en dur pour utiliser la <a href="https://en.wikipedia.org/wiki/P-384" title="P-384 on Wikipedia">courbe elliptique NIST
P-384</a>.</p>
<p>La première étape consiste à réinitialiser toutes vos YubiKeys :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>offline-pki<span class="w"> </span>yubikey<span class="w"> </span>reset
<span class="go">This will reset the connected YubiKey. Are you sure? [y/N]: y</span>
<span class="go">New PIN code:</span>
<span class="go">Repeat for confirmation:</span>
<span class="go">New PUK code:</span>
<span class="go">Repeat for confirmation:</span>
<span class="go">New management key ('.' to generate a random one):</span>
<span class="go">WARNING[pki-yubikey] Using random management key: e8ffdce07a4e3bd5c0d803aa3948a9c36cfb86ed5a2d5cf533e97b088ae9e629</span>
<span class="go">INFO[pki-yubikey]  0: Yubico YubiKey OTP+FIDO+CCID 00 00</span>
<span class="go">INFO[pki-yubikey] SN: 23854514</span>
<span class="go">INFO[yubikit.management] Device config written</span>
<span class="go">INFO[yubikit.piv] PIV application data reset performed</span>
<span class="go">INFO[yubikit.piv] Management key set</span>
<span class="go">INFO[yubikit.piv] New PUK set</span>
<span class="go">INFO[yubikit.piv] New PIN set</span>
<span class="go">INFO[pki-yubikey] YubiKey reset successful!</span>
</code></pre></div>

<p>Ensuite, générez l'autorité de certification racine et créez autant de copies
que vous voulez :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>offline-pki<span class="w"> </span>certificate<span class="w"> </span>root<span class="w"> </span>--permitted<span class="w"> </span>example.com
<span class="go">Management key for Root X:</span>
<span class="go">Plug YubiKey "Root X"...</span>
<span class="go">INFO[pki-yubikey]  0: Yubico YubiKey CCID 00 00</span>
<span class="go">INFO[pki-yubikey] SN: 23854514</span>
<span class="go">INFO[yubikit.piv] Data written to object slot 0x5fc10a</span>
<span class="go">INFO[yubikit.piv] Certificate written to slot 9C (SIGNATURE), compression=True</span>
<span class="go">INFO[yubikit.piv] Private key imported in slot 9C (SIGNATURE) of type ECCP384</span>
<span class="go">Copy root certificate to another YubiKey? [y/N]: y</span>
<span class="go">Plug YubiKey "Root X"...</span>
<span class="go">INFO[pki-yubikey]  0: Yubico YubiKey CCID 00 00</span>
<span class="go">INFO[pki-yubikey] SN: 23854514</span>
<span class="go">INFO[yubikit.piv] Data written to object slot 0x5fc10a</span>
<span class="go">INFO[yubikit.piv] Certificate written to slot 9C (SIGNATURE), compression=True</span>
<span class="go">INFO[yubikit.piv] Private key imported in slot 9C (SIGNATURE) of type ECCP384</span>
<span class="go">Copy root certificate to another YubiKey? [y/N]: n</span>
</code></pre></div>

<p>Vous pouvez examiner le résultat :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>offline-pki<span class="w"> </span>yubikey<span class="w"> </span>info
<span class="go">INFO[pki-yubikey]  0: Yubico YubiKey CCID 00 00</span>
<span class="go">INFO[pki-yubikey] SN: 23854514</span>
<span class="go">INFO[pki-yubikey] Slot 9C (SIGNATURE):</span>
<span class="go">INFO[pki-yubikey]   Private key type: ECCP384</span>
<span class="go">INFO[pki-yubikey]   Public key:</span>
<span class="go">INFO[pki-yubikey]     Algorithm:  secp384r1</span>
<span class="go">INFO[pki-yubikey]     Issuer:     CN=Root CA</span>
<span class="go">INFO[pki-yubikey]     Subject:    CN=Root CA</span>
<span class="go">INFO[pki-yubikey]     Serial:     1</span>
<span class="go">INFO[pki-yubikey]     Not before: 2024-07-05T18:17:19+00:00</span>
<span class="go">INFO[pki-yubikey]     Not after:  2044-06-30T18:17:19+00:00</span>
<span class="go">INFO[pki-yubikey]     PEM:</span>
<span class="go">-----BEGIN CERTIFICATE-----</span>
<span class="go">MIIBcjCB+aADAgECAgEBMAoGCCqGSM49BAMDMBIxEDAOBgNVBAMMB1Jvb3QgQ0Ew</span>
<span class="go">HhcNMjQwNzA1MTgxNzE5WhcNNDQwNjMwMTgxNzE5WjASMRAwDgYDVQQDDAdSb290</span>
<span class="go">IENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERg3Vir6cpEtB8Vgo5cAyBTkku/4w</span>
<span class="go">kXvhWlYZysz7+YzTcxIInZV6mpw61o8W+XbxZV6H6+3YHsr/IeigkK04/HJPi6+i</span>
<span class="go">zU5WJHeBJMqjj2No54Nsx6ep4OtNBMa/7T9foyMwITAPBgNVHRMBAf8EBTADAQH/</span>
<span class="go">MA4GA1UdDwEB/wQEAwIBhjAKBggqhkjOPQQDAwNoADBlAjEAwYKy/L8leJyiZSnn</span>
<span class="go">xrY8xv8wkB9HL2TEAI6fC7gNc2bsISKFwMkyAwg+mKFKN2w7AjBRCtZKg4DZ2iUo</span>
<span class="go">6c0BTXC9a3/28V5aydZj6rvx0JqbF/Ln5+RQL6wFMLoPIvCIiCU=</span>
<span class="go">-----END CERTIFICATE-----</span>
</code></pre></div>

<p>Ensuite, vous pouvez créer un certificat intermédiaire avec <code>offline-pki yubikey
intermediate</code> et l'utiliser pour signer des certificats en fournissant leur <abbr title="Certificate Signing Request">CSR</abbr>
à <code>offline-pki certificate sign</code>. Soyez prudent et inspectez la <abbr title="Certificate Signing Request">CSR</abbr> avant de la
signer, car seul le nom du sujet peut être remplacé. Consultez la
<a href="https://github.com/vincentbernat/offline-pki?tab=readme-ov-file#operations">documentation</a> pour plus de détails. Obtenez les options disponibles en
utilisant l'option <code>--help</code>.</p>
<h1 id="la-partie-materielle">La partie matérielle</h1>
<p>Pour garantir que les opérations sur les autorités de certification racine et
intermédiaire sont isolées du réseau, une solution économique consiste à
utiliser une carte ARM64. Le <a href="https://libre.computer/products/aml-s905x-cc-v2/" title="Sweet Potato AML-S905X-CC-V2">Libre Computer Sweet Potato</a> est une
alternative plus ouverte au célèbre Raspberry Pi<sup id="fnref:root"><a class="footnote-ref" href="#fn:root">1</a></sup>.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 800px"><span class="lf-media-inner" style="padding-bottom: 70.000%"><img alt="Ordinateur monocarte Libre Computer Sweet Potato s'appuyant sur le SOC Amlogic&#10;S905X" src="https://media-bernat-ch.b-cdn.net/images/sweet-potato@1x.34ce59fcbcf47f.jpg" srcset="https://media-bernat-ch.b-cdn.net/images/sweet-potato@1x.34ce59fcbcf47f.jpg 1006w,https://media-bernat-ch.b-cdn.net/images/sweet-potato@2x.e575728a8cf197.jpg 1600w" sizes="auto, (max-width: 800px) 100vw, 800px" width="800" height="560" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABkAAAARhAQMAAABArdteAAAAA1BMVEXGnW8rul+2AAAA8UlEQVR42u3BMQEAAADCoPVPbQo/oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuBhwVgABAgn2ogAAAABJRU5ErkJggg==)"/></span></div><figcaption>SBC Libre Computer Sweet Potato, alimenté par le SOC AML-S905X</figcaption></figure>
<p>J'interagis avec lui via un <a href="https://www.amazon.fr/dp/B07WX2DSVB" title="Convertisseur USB vers série TTL sur Amazon">convertisseur USB vers série</a> :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>tio<span class="w"> </span>/dev/ttyUSB0
<span class="go">[16:40:44.546] tio v3.7</span>
<span class="go">[16:40:44.546] Press ctrl-t q to quit</span>
<span class="go">[16:40:44.555] Connected to /dev/ttyUSB0</span>
<span class="go">GXL:BL1:9ac50e:bb16dc;FEAT:ADFC318C:0;POC:1;RCY:0;SPI:0;0.0;CHK:0;</span>
<span class="go">TE: 36574</span>

<span class="go">BL2 Built : 15:21:18, Aug 28 2019. gxl g1bf2b53 - luan.yuan@droid15-sz</span>

<span class="go">set vcck to 1120 mv</span>
<span class="go">set vddee to 1000 mv</span>
<span class="go">Board ID = 4</span>
<span class="go">CPU clk: 1200MHz</span>
<span class="go">[…]</span>
</code></pre></div>

<h1 id="lassemblage-nix">L'assemblage Nix</h1>
<p>Pour coller tout ensemble, j'utilise <em>Nix</em> avec un <a href="https://github.com/vincentbernat/offline-pki/blob/main/flake.nix">Flake</a> fournissant :</p>
<ul>
<li>un paquet pour l'application <code>offline-pki</code>, avec la complétion pour le shell ;</li>
<li>un shell de développement, incluant une version éditable de l'application <code>offline-pki</code> ;</li>
<li>un module NixOS pour configurer la <abbr title="Public Key Infrastructure">PKI</abbr>, réinitialisant le système à chaque démarrage ;</li>
<li>une image QEMU pour les tests ;</li>
<li>une image de carte SD à utiliser sur le <em>Sweet Potato</em> ou une autre carte ARM64.</li>
</ul>
<div class="language-bash codehilite"><pre><span/><code><span class="c1"># Exécuter localement l'application</span>
nix<span class="w"> </span>run<span class="w"> </span>github:vincentbernat/offline-pki<span class="w"> </span>--<span class="w"> </span>--help
<span class="c1"># Exécuter l'application dans une machine virtuelle QEMU</span>
nix<span class="w"> </span>run<span class="w"> </span>github:vincentbernat/offline-pki<span class="se">\#</span>qemu
<span class="c1"># Construire l'image d'une carte SD pour Sweet Potato ou un Raspberry Pi</span>
nix<span class="w"> </span>build<span class="w"> </span>--system<span class="w"> </span>aarch64-linux<span class="w"> </span>github:vincentbernat/offline-pki<span class="se">\#</span>sdcard.potato
nix<span class="w"> </span>build<span class="w"> </span>--system<span class="w"> </span>aarch64-linux<span class="w"> </span>github:vincentbernat/offline-pki<span class="se">\#</span>sdcard.generic
<span class="c1"># Obtenir un shell de développement avec l'application</span>
nix<span class="w"> </span>develop<span class="w"> </span>github:vincentbernat/offline-pki
</code></pre></div>

<div class="footnote">
<hr/>
<ol>
<li id="fn:root">
<p>La clé pour l'autorité de certification racine n'est pas générée par la
YubiKey. L'utilisation d'un ordinateur isolé est d'autant plus importante.
Mettez-le dans un coffre-fort avec les YubiKeys quand vous avez terminé ! <a class="footnote-backref" href="#fnref:root" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Personnaliser les modules de Caddy avec Nix</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2024-caddy-nix-modules" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2024-caddy-nix-modules#isso-thread" rel="replies" type="text/html"/>
    <updated>2024-11-11T06:35:50Z</updated>
    <id>http://www.luffy.cx/fr/blog/2024-caddy-nix-modules.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p><a href="https://caddyserver.com/" title="Caddy Web Server">Caddy</a> est un serveur web libre écrit en Go. Il gère automatiquement les
certificats TLS et propose une syntaxe de configuration simple. Les utilisateurs
peuvent étendre ses fonctionnalités via des <a href="https://caddyserver.com/docs/modules/" title="All registered Caddy plugins">modules</a> pour ajouter, par
exemple, la <a href="https://github.com/mholt/caddy-ratelimit" title="HTTP rate limiting module for Caddy">limitation de débit</a>, la <a href="https://github.com/caddyserver/cache-handler" title="Distributed HTTP caching module for Caddy">mise en cache</a> et
l'<a href="https://github.com/invzhi/caddy-docker-upstreams" title="Docker dynamic upstreams for Caddy">intégration à Docker</a>.</p>
<p>Bien que <em>Caddy</em> soit disponible dans <a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ca/caddy/package.nix">Nixpkgs</a>, l'ajout de modules
supplémentaires <del>n'est</del> <ins>n'était</ins> pas simple<sup id="fnref:simple"><a class="footnote-ref" href="#fn:simple">1</a></sup>. Le
processus de compilation nécessite un accès Internet, ce que <a href="https://zero-to-nix.com/concepts/hermeticity/" title="Zero to Nix: hermeticity">Nix refuse pendant
la compilation</a> pour garantir la
<a href="https://zero-to-nix.com/concepts/reproducibility/" title="Zero to Nix: reproducibility">reproductibilité</a>. Lorsqu'on essaie de construire la
dérivation suivante en utilisant <a href="https://github.com/caddyserver/xcaddy" title="Build Caddy with plugins">xcaddy</a>, un outil pour compiler Caddy avec
des modules supplémentaires, cela échoue avec cette erreur : <code>dial tcp: lookup
proxy.golang.org on [::1]:53: connection refused</code>.</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> pkgs <span class="p">}:</span>
pkgs<span class="o">.</span>stdenv<span class="o">.</span>mkDerivation <span class="p">{</span>
  <span class="ss">name</span> <span class="o">=</span> <span class="s2">"caddy-with-xcaddy"</span><span class="p">;</span>
  <span class="ss">nativeBuildInputs</span> <span class="o">=</span> <span class="k">with</span> pkgs<span class="p">;</span> <span class="p">[</span> go xcaddy cacert <span class="p">];</span>
  <span class="ss">unpackPhase</span> <span class="o">=</span> <span class="s2">"true"</span><span class="p">;</span>
  <span class="ss">buildPhase</span> <span class="o">=</span>
    <span class="s s-Multiline">''</span>
<span class="s s-Multiline">      xcaddy build --with github.com/caddy-dns/powerdns@v1.0.1</span>
<span class="s s-Multiline">    ''</span><span class="p">;</span>
  <span class="ss">installPhase</span> <span class="o">=</span> <span class="s s-Multiline">''</span>
<span class="s s-Multiline">    mkdir -p $out/bin</span>
<span class="s s-Multiline">    cp caddy $out/bin</span>
<span class="s s-Multiline">  ''</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<p>Les dérivations à sortie fixe (<em>fixed-output derivations</em>) constituent une
exception à cette règle et obtiennent un accès réseau pendant la compilation.
Elles doivent spécifier leur hash de sortie. Par exemple, la fonction <code>fetchurl</code>
produit une dérivation à sortie fixe :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span> stdenv<span class="p">,</span> fetchurl <span class="p">}:</span>
stdenv<span class="o">.</span>mkDerivation <span class="k">rec</span> <span class="p">{</span>
  <span class="ss">pname</span> <span class="o">=</span> <span class="s2">"hello"</span><span class="p">;</span>
  <span class="ss">version</span> <span class="o">=</span> <span class="s2">"2.12.1"</span><span class="p">;</span>
  <span class="ss">src</span> <span class="o">=</span> fetchurl <span class="p">{</span>
    <span class="ss">url</span> <span class="o">=</span> <span class="s2">"mirror://gnu/hello/hello-</span><span class="si">${</span>version<span class="si">}</span><span class="s2">.tar.gz"</span><span class="p">;</span>
    <span class="ss">hash</span> <span class="o">=</span> <span class="s2">"sha256-jZkUKv2SV28wsM18tCqNxoCZmLxdYH2Idh9RLibH2yA="</span><span class="p">;</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<p>Pour créer une dérivation à sortie fixe, vous devez définir l'<a href="https://nix.dev/manual/nix/2.22/language/advanced-attributes.html#adv-attr-outputHash">attribut
<code>outputHash</code></a>. L'exemple ci-dessous montre comment obtenir le code
source de <em>Caddy</em>, avec certains modules activés, comme une dérivation à sortie
fixe en utilisant <code>xcaddy</code> et <code>go mod vendor</code>.</p>
<div class="language-nix codehilite"><pre><span/><code>pkgs<span class="o">.</span>stdenvNoCC<span class="o">.</span>mkDerivation <span class="k">rec</span> <span class="p">{</span>
  <span class="ss">pname</span> <span class="o">=</span> <span class="s2">"caddy-src-with-xcaddy"</span><span class="p">;</span>
  <span class="ss">version</span> <span class="o">=</span> <span class="s2">"2.8.4"</span><span class="p">;</span>
  <span class="ss">nativeBuildInputs</span> <span class="o">=</span> <span class="k">with</span> pkgs<span class="p">;</span> <span class="p">[</span> go xcaddy cacert <span class="p">];</span>
  <span class="ss">unpackPhase</span> <span class="o">=</span> <span class="s2">"true"</span><span class="p">;</span>
  <span class="ss">buildPhase</span> <span class="o">=</span>
    <span class="s s-Multiline">''</span>
<span class="s s-Multiline">      export GOCACHE=$TMPDIR/go-cache</span>
<span class="s s-Multiline">      export GOPATH="$TMPDIR/go"</span>
<span class="s s-Multiline">      XCADDY_SKIP_BUILD=1 TMPDIR="$PWD" \</span>
<span class="s s-Multiline">        xcaddy build v</span><span class="si">${</span>version<span class="si">}</span><span class="s s-Multiline"> --with github.com/caddy-dns/powerdns@v1.0.1</span>
<span class="s s-Multiline">      (cd buildenv* &amp;&amp; go mod vendor)</span>
<span class="s s-Multiline">    ''</span><span class="p">;</span>
  <span class="ss">installPhase</span> <span class="o">=</span> <span class="s s-Multiline">''</span>
<span class="s s-Multiline">    mv buildenv* $out</span>
<span class="s s-Multiline">  ''</span><span class="p">;</span>

<span class="hll">  <span class="ss">outputHash</span> <span class="o">=</span> <span class="s2">"sha256-F/jqR4iEsklJFycTjSaW8B/V3iTGqqGOzwYBUXxRKrc="</span><span class="p">;</span>
</span><span class="hll">  <span class="ss">outputHashAlgo</span> <span class="o">=</span> <span class="s2">"sha256"</span><span class="p">;</span>
</span><span class="hll">  <span class="ss">outputHashMode</span> <span class="o">=</span> <span class="s2">"recursive"</span><span class="p">;</span>
</span><span class="p">}</span>
</code></pre></div>

<p>Avec une dérivation à sortie fixe, c'est à nous de nous assurer que la sortie
est toujours la même :</p>
<ul>
<li><code>xcaddy</code> ne compile pas le programme et conserve le code source<sup id="fnref:xcaddy"><a class="footnote-ref" href="#fn:xcaddy">2</a></sup> ;</li>
<li>nous spécifions la version de Caddy que nous voulons construire ;</li>
<li>nous spécifions la version de chaque module demandé.</li>
</ul>
<p>Vous pouvez utiliser cette dérivation pour remplacer l'attribut <code>src</code> dans
<code>pkgs.caddy</code> :</p>
<div class="language-nix codehilite"><pre><span/><code>pkgs<span class="o">.</span>caddy<span class="o">.</span>overrideAttrs <span class="p">(</span>prev<span class="p">:</span> <span class="p">{</span>
  <span class="ss">src</span> <span class="o">=</span> pkgs<span class="o">.</span>stdenvNoCC<span class="o">.</span>mkDerivation <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">};</span>
  <span class="ss">vendorHash</span> <span class="o">=</span> <span class="no">null</span><span class="p">;</span>
  <span class="ss">subPackages</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"."</span> <span class="p">];</span>
<span class="p">});</span>
</code></pre></div>

<p>Consultez l'exemple complet dans le <a href="https://github.com/vincentbernat/caddy-nix/blob/main/examples/xcaddy-src/flake.nix">dépôt Git</a>. Pour une
intégration dans un <a href="https://zero-to-nix.com/concepts/flakes/" title="Zero to Nix: flakes">Flake</a>, vous pouvez utiliser <code>github:vincentbernat/caddy-nix</code> comme
surcouche à <em>Nixpkgs</em> :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span>
  <span class="ss">inputs</span> <span class="o">=</span> <span class="p">{</span>
    nixpkgs<span class="o">.</span><span class="ss">url</span> <span class="o">=</span> <span class="s2">"nixpkgs"</span><span class="p">;</span>
    flake-utils<span class="o">.</span><span class="ss">url</span> <span class="o">=</span> <span class="s2">"github:numtide/flake-utils"</span><span class="p">;</span>
    caddy<span class="o">.</span><span class="ss">url</span> <span class="o">=</span> <span class="s2">"github:vincentbernat/caddy-nix"</span><span class="p">;</span>
  <span class="p">};</span>
  <span class="ss">outputs</span> <span class="o">=</span> <span class="p">{</span> self<span class="p">,</span> nixpkgs<span class="p">,</span> flake-utils<span class="p">,</span> caddy <span class="p">}:</span>
    flake-utils<span class="o">.</span>lib<span class="o">.</span>eachDefaultSystem <span class="p">(</span>system<span class="p">:</span>
      <span class="k">let</span>
        <span class="ss">pkgs</span> <span class="o">=</span> <span class="nb">import</span> nixpkgs <span class="p">{</span>
          <span class="k">inherit</span> system<span class="p">;</span>
          <span class="ss">overlays</span> <span class="o">=</span> <span class="p">[</span> caddy<span class="o">.</span>overlays<span class="o">.</span>default <span class="p">];</span>
        <span class="p">};</span>
      <span class="k">in</span>
      <span class="p">{</span>
        <span class="ss">packages</span> <span class="o">=</span> <span class="p">{</span>
          <span class="ss">default</span> <span class="o">=</span> pkgs<span class="o">.</span>caddy<span class="o">.</span>withPlugins <span class="p">{</span>
            <span class="ss">plugins</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"github.com/caddy-dns/powerdns@v1.0.1"</span> <span class="p">];</span>
            <span class="ss">hash</span> <span class="o">=</span> <span class="s2">"sha256-F/jqR4iEsklJFycTjSaW8B/V3iTGqqGOzwYBUXxRKrc="</span><span class="p">;</span>
          <span class="p">};</span>
        <span class="p">};</span>
      <span class="p">});</span>
<span class="p">}</span>
</code></pre></div>

<div class="admonition">
<p class="admonition-title">Mise à jour (11.2024)</p>
<p>Cela ne fonctionne pas avec Nixpkgs 24.05 car <a href="https://github.com/nixOS/nixpkgs/commit/eed069a5bc40ba4d871de7700c7eb8d592e98cb6">cette
modification</a>
est nécessaire pour pouvoir changer la valeur de l'attribut <code>vendorHash</code>.</p>
</div>
<div class="admonition">
<p class="admonition-title">Mise à jour (05.2025)</p>
<p>Depuis Nixpkgs 25.05, il n'est plus nécessaire d'ajouter cette surcouche.
Jetez un œil à <a href="https://github.com/vincentbernat/caddy-nix/blob/main/examples/nixpkgs-25.05/flake.nix">l'exemple complet</a>.</p>
</div>
<div class="footnote">
<hr/>
<ol>
<li id="fn:simple">
<p>C'est une <a href="https://github.com/NixOS/nixpkgs/issues/14671" title="caddy: add all addons #14671">demande</a> depuis un certain temps. Une
<a href="https://github.com/NixOS/nixpkgs/pull/317881" title="caddy: add support for compiling with Caddy modules (plugins) #317881">solution proposée</a> a été rejetée. Celle décrite dans cet
article est un peu différente et je l'ai <a href="https://github.com/NixOS/nixpkgs/pull/358586" title="caddy: add suport for compiling Caddy with plugins #358586">proposée</a> en
remplacement. Elle a été acceptée. <a class="footnote-backref" href="#fnref:simple" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:xcaddy">
<p>Ce n'est pas parfait : si le code source produit par <code>xcaddy</code> change,
le hash changerait et la compilation échouerait. <a class="footnote-backref" href="#fnref:xcaddy" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Gérer une infrastructure avec Terraform, CDKTF et NixOS</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2022-cdktf-nixos" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2022-cdktf-nixos#isso-thread" rel="replies" type="text/html"/>
    <updated>2022-12-26T14:18:38Z</updated>
    <id>http://www.luffy.cx/fr/blog/2022-cdktf-nixos.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>Il y a quelques années, j'ai réduit mon infrastructure personnelle au strict
minimum. Jusqu'en 2018, elle représentait une douzaine de conteneurs tournant
sur un seul serveur <a href="https://www.hetzner.com/" title="Hetzner: dedicated servers, cloud, storage &amp; hosting">Hetzner</a><sup id="fnref:hetzner"><a class="footnote-ref" href="#fn:hetzner">1</a></sup>. J'ai migré mon courriel vers
<a href="https://www.fastmail.com/">Fastmail</a> et mes zones DNS vers <a href="https://www.gandi.net/en" title="Gandi: Domain Names, Web Hosting, Mail">Gandi</a>. Il ne me restait plus que mon blog
à héberger. À ce jour, ma petite infrastructure est composée de 4 machines
virtuelles exécutant <a href="https://nixos.org/" title="Nix: reproducible builds and deployments">NixOS</a> sur <em>Hetzner Cloud</em> et <a href="https://www.vultr.com/" title="Vultr.com: SSD VPS Servers, Cloud Servers and Cloud Hosting">Vultr</a>, d'une poignée
de zones DNS sur <em>Gandi</em> et <a href="https://aws.amazon.com/route53/" title="Amazon AWS: Route 53">Route 53</a>, et de quelques distributions
<a href="https://aws.amazon.com/cloudfront/" title="Amazon AWS: CloudFront">CloudFront</a>. Elle est gérée par <a href="https://developer.hashicorp.com/terraform/cdktf" title="Cloud Development Kit for Terraform">CDK pour Terraform</a>
(<abbr title="Cloud Development Kit for Terraform">CDKTF</abbr>), tandis que les déploiements de <em>NixOS</em> sont gérés par <a href="https://github.com/NixOS/nixops" title="NixOps:  tool for deploying to NixOS machines in a network or cloud">NixOps</a>.</p>
<p>Dans cet article, je présente brièvement <em>Terraform</em>, <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em> et l'écosystème
<em>Nix</em>. J'explique également comment utiliser <em>Nix</em> pour accéder à ces outils
dans votre shell afin de les utiliser rapidement.</p>
<div class="admonition">
<p class="admonition-title">Mise à jour (10.2025)</p>
<p><em>HashiCorp</em> <a href="https://www.hashicorp.com/blog/hashicorp-adopts-business-source-license">a opté pour la Business Source License</a> pour tous ces logiciels. C'est une déception,
notamment pour <em>Terraform</em> qui est un composant clé pour automatiser les
infrastructures et qui a bénéficié d'une large communauté. Il existe
désormais un fork communautaire, <a href="https://opentofu.org/" title="The open source infrastructure as code tool">OpenTofu</a>, mais celui-ci ne couvre pas
<em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em>. Le projet a <a href="https://github.com/opentofu/opentofu/issues/1335#issuecomment-3365674369">indiqué</a> qu'il n'avait pas le temps pour travailler
sur la compatibilité avec <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em>.</p>
</div>
<div class="admonition">
<p class="admonition-title">Mise à jour (12.2025)</p>
<p>Le développement de <a href="https://github.com/hashicorp/terraform-cdk/commit/a0c56138e2b4c7b6c128bd7ba08a8e317df852af"><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr> s'est arrêté</a>. Cela m'agace prodigieusement. Je
suppose que je vais devoir regarder de nouveau vers <a href="https://www.pulumi.com/" title="Pulumi: universal infrastructure as code">Pulumi</a>.</p>
</div>
<div class="admonition">
<p class="admonition-title">Mise à jour (10.2026)</p>
<p>Finalement, je suis passé à <a href="https://opentofu.org/" title="The open source infrastructure as code tool">OpenTofu</a> et à <a href="https://cdktn.io/" title="CDK Terrain: infrastructure as code with programming languages">CDK Terrain</a>, un fork
communautaire de <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em>.</p>
</div>
<div class="toc">
<ul>
<li><a href="#cdktf-infrastructure-en-tant-que-code">CDKTF : infrastructure en tant que code</a><ul>
<li><a href="#gerer-des-serveurs">Gérer des serveurs</a></li>
<li><a href="#gerer-des-enregistrements-dns">Gérer des enregistrements DNS</a></li>
<li><a href="#a-propos-de-pulumi">À propos de Pulumi</a></li>
</ul>
</li>
<li><a href="#nixos-nixops">NixOS &amp; NixOps</a><ul>
<li><a href="#nixos-distribution-linux-declarative">NixOS : distribution Linux déclarative</a></li>
<li><a href="#nixops-outil-de-deploiement-pour-nixos">NixOps : outil de déploiement pour NixOS</a></li>
</ul>
</li>
<li><a href="#connecter-le-tout-avec-nix">Connecter le tout avec Nix</a><ul>
<li><a href="#courte-introduction-aux-flakes-nix">Courte introduction aux « flakes » Nix</a></li>
<li><a href="#nix-et-cdktf">Nix et CDKTF</a></li>
<li><a href="#nixops">NixOps</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="cdktf-infrastructure-en-tant-que-code"><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr> : infrastructure en tant que code</h1>
<p><a href="https://developer.hashicorp.com/terraform">Terraform</a> est un outil d'« infrastructure en tant que code ». Vous pouvez
définir votre infrastructure en déclarant des ressources avec le <a href="https://developer.hashicorp.com/terraform/language/modules">langage
<abbr title="HashiCorp Configuration Language">HCL</abbr></a>. Ce dernier possède quelques fonctionnalités supplémentaires,
comme des boucles permettant de déclarer plusieurs ressources à partir d'une
liste, des fonctions intégrées que vous pouvez appeler dans les expressions et
l'expansion de variables dans les chaînes de caractères. <em>Terraform</em> s'appuie
sur un large <a href="https://registry.terraform.io/browse/providers">ensemble de fournisseurs</a> pour gérer les
ressources.</p>
<h2 id="gerer-des-serveurs">Gérer des serveurs</h2>
<p>Voici un court exemple utilisant le <a href="https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs">fournisseur pour Hetzner Cloud</a> pour créer une machine virtuelle :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="kr">variable</span><span class="w"> </span><span class="nv">"hcloud_token"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">sensitive</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="no">true</span>
<span class="p">}</span>
<span class="kr">provider</span><span class="w"> </span><span class="nv">"hcloud"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">var.hcloud_token</span>
<span class="p">}</span>

<span class="kr">resource</span><span class="w"> </span><span class="nc">"hcloud_server"</span><span class="w"> </span><span class="nv">"web03"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"web03"</span>
<span class="w">  </span><span class="na">server_type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"cpx11"</span>
<span class="w">  </span><span class="na">image</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"debian-11"</span>
<span class="w">  </span><span class="na">datacenter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"nbg1-dc3"</span>
<span class="p">}</span>

<span class="kr">resource</span><span class="w"> </span><span class="nc">"hcloud_rdns"</span><span class="w"> </span><span class="nv">"rdns4-web03"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">server_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">hcloud_server.web03.id</span>
<span class="w">  </span><span class="na">ip_address</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">hcloud_server.web03.ipv4_address</span>
<span class="w">  </span><span class="na">dns_ptr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"web03.luffy.cx"</span>
<span class="p">}</span>

<span class="kr">resource</span><span class="w"> </span><span class="nc">"hcloud_rdns"</span><span class="w"> </span><span class="nv">"rdns6-web03"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">server_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">hcloud_server.web03.id</span>
<span class="w">  </span><span class="na">ip_address</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">hcloud_server.web03.ipv6_address</span>
<span class="w">  </span><span class="na">dns_ptr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"web03.luffy.cx"</span>
<span class="p">}</span>
</code></pre></div>

<p>L'expressivité de <em><abbr title="HashiCorp Configuration Language">HCL</abbr></em> est assez limitée et je trouve qu'un langage généraliste
est plus pratique pour décrire les ressources. C'est là qu'intervient <em>CDK pour
Terraform</em> : vous pouvez gérer votre infrastructure à l'aide de votre langage de
programmation préféré, notamment TypeScript, Go et Python. Voici l'exemple
précédent utilisant <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em> et TypeScript :</p>
<div class="language-typescript codehilite"><pre><span/><code><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">App</span><span class="p">,</span><span class="w"> </span><span class="nx">TerraformStack</span><span class="p">,</span><span class="w"> </span><span class="nx">Fn</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">"cdktf"</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">HcloudProvider</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">"./.gen/providers/hcloud/provider"</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">hcloud</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">"./.gen/providers/hcloud"</span><span class="p">;</span>

<span class="kd">class</span><span class="w"> </span><span class="nx">MyStack</span><span class="w"> </span><span class="k">extends</span><span class="w"> </span><span class="nx">TerraformStack</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kr">constructor</span><span class="p">(</span><span class="nx">scope</span><span class="o">:</span><span class="w"> </span><span class="kt">Construct</span><span class="p">,</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">super</span><span class="p">(</span><span class="nx">scope</span><span class="p">,</span><span class="w"> </span><span class="nx">name</span><span class="p">);</span>

<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="nx">hcloudToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">TerraformVariable</span><span class="p">(</span><span class="k">this</span><span class="p">,</span><span class="w"> </span><span class="s2">"hcloudToken"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="kr">type</span><span class="o">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span>
<span class="w">      </span><span class="nx">sensitive</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
<span class="w">    </span><span class="p">});</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="nx">hcloudProvider</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">HcloudProvider</span><span class="p">(</span><span class="k">this</span><span class="p">,</span><span class="w"> </span><span class="s2">"hcloud"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="nx">token</span><span class="o">:</span><span class="w"> </span><span class="kt">hcloudToken.value</span><span class="p">,</span>
<span class="w">    </span><span class="p">});</span>

<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="nx">web03</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">hcloud</span><span class="p">.</span><span class="nx">server</span><span class="p">.</span><span class="nx">Server</span><span class="p">(</span><span class="k">this</span><span class="p">,</span><span class="w"> </span><span class="s2">"web03"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="s2">"web03"</span><span class="p">,</span>
<span class="w">      </span><span class="nx">serverType</span><span class="o">:</span><span class="w"> </span><span class="s2">"cpx11"</span><span class="p">,</span>
<span class="w">      </span><span class="nx">image</span><span class="o">:</span><span class="w"> </span><span class="s2">"debian-11"</span><span class="p">,</span>
<span class="w">      </span><span class="nx">datacenter</span><span class="o">:</span><span class="w"> </span><span class="s2">"nbg1-dc3"</span><span class="p">,</span>
<span class="w">      </span><span class="nx">provider</span><span class="o">:</span><span class="w"> </span><span class="kt">hcloudProvider</span><span class="p">,</span>
<span class="w">    </span><span class="p">});</span>
<span class="w">    </span><span class="ow">new</span><span class="w"> </span><span class="nx">hcloud</span><span class="p">.</span><span class="nx">rdns</span><span class="p">.</span><span class="nx">Rdns</span><span class="p">(</span><span class="k">this</span><span class="p">,</span><span class="w"> </span><span class="s2">"rdns4-web03"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="nx">serverId</span><span class="o">:</span><span class="w"> </span><span class="kt">Fn.tonumber</span><span class="p">(</span><span class="nx">web03</span><span class="p">.</span><span class="nx">id</span><span class="p">),</span>
<span class="w">      </span><span class="nx">ipAddress</span><span class="o">:</span><span class="w"> </span><span class="kt">web03.ipv4Address</span><span class="p">,</span>
<span class="w">      </span><span class="nx">dnsPtr</span><span class="o">:</span><span class="w"> </span><span class="s2">"web03.luffy.cx"</span><span class="p">,</span>
<span class="w">      </span><span class="nx">provider</span><span class="o">:</span><span class="w"> </span><span class="kt">hcloudProvider</span><span class="p">,</span>
<span class="w">    </span><span class="p">});</span>
<span class="w">    </span><span class="ow">new</span><span class="w"> </span><span class="nx">hcloud</span><span class="p">.</span><span class="nx">rdns</span><span class="p">.</span><span class="nx">Rdns</span><span class="p">(</span><span class="k">this</span><span class="p">,</span><span class="w"> </span><span class="s2">"rdns6-web03"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="nx">serverId</span><span class="o">:</span><span class="w"> </span><span class="kt">Fn.tonumber</span><span class="p">(</span><span class="nx">web03</span><span class="p">.</span><span class="nx">id</span><span class="p">),</span>
<span class="w">      </span><span class="nx">ipAddress</span><span class="o">:</span><span class="w"> </span><span class="kt">web03.ipv6Address</span><span class="p">,</span>
<span class="w">      </span><span class="nx">dnsPtr</span><span class="o">:</span><span class="w"> </span><span class="s2">"web03.luffy.cx"</span><span class="p">,</span>
<span class="w">      </span><span class="nx">provider</span><span class="o">:</span><span class="w"> </span><span class="kt">hcloudProvider</span><span class="p">,</span>
<span class="w">    </span><span class="p">});</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>

<span class="kd">const</span><span class="w"> </span><span class="nx">app</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">App</span><span class="p">();</span>
<span class="ow">new</span><span class="w"> </span><span class="nx">MyStack</span><span class="p">(</span><span class="nx">app</span><span class="p">,</span><span class="w"> </span><span class="s2">"cdktf-take1"</span><span class="p">);</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">synth</span><span class="p">();</span>
</code></pre></div>

<p>La commande de <code>cdktf synth</code> génère un fichier de configuration pour
<em>Terraform</em>, <code>terraform plan</code> prévisualise les changements et <code>terraform apply</code>
les applique. Maintenant que vous disposez d'un langage généraliste, vous pouvez
utiliser des fonctions.</p>
<h2 id="gerer-des-enregistrements-dns">Gérer des enregistrements DNS</h2>
<p>Si l'utilisation de <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em> pour 4 serveurs web peut sembler un peu exagérée, il
en va tout autrement lorsqu'il s'agit de gérer quelques zones DNS. Avec
<a href="https://dnscontrol.org/" title="DNSControl: Synchronize your DNS to multiple providers from a simple DSL">DNSControl</a>, qui utilise JavaScript comme langage, j'ai pu définir la zone
<code>bernat.ch</code> avec ce bout de code :</p>
<div class="language-javascript codehilite"><pre><span/><code><span class="nx">D</span><span class="p">(</span><span class="s2">"bernat.ch"</span><span class="p">,</span><span class="w"> </span><span class="nx">REG_NONE</span><span class="p">,</span><span class="w"> </span><span class="nx">DnsProvider</span><span class="p">(</span><span class="nx">DNS_BIND</span><span class="p">,</span><span class="w"> </span><span class="mf">0</span><span class="p">),</span><span class="w"> </span><span class="nx">DnsProvider</span><span class="p">(</span><span class="nx">DNS_GANDI</span><span class="p">),</span>
<span class="w">  </span><span class="nx">DefaultTTL</span><span class="p">(</span><span class="s1">'2h'</span><span class="p">),</span>
<span class="w">  </span><span class="nx">FastMailMX</span><span class="p">(</span><span class="s1">'bernat.ch'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">subdomains</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s1">'vincent'</span><span class="p">]}),</span>
<span class="w">  </span><span class="nx">WebServers</span><span class="p">(</span><span class="s1">'@'</span><span class="p">),</span>
<span class="w">  </span><span class="nx">WebServers</span><span class="p">(</span><span class="s1">'vincent'</span><span class="p">);</span>
</code></pre></div>

<p>Cela produit 38 enregistrements. Avec <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em>, j'écris :</p>
<div class="language-typescript codehilite"><pre><span/><code><span class="ow">new</span><span class="w"> </span><span class="nx">Route53Zone</span><span class="p">(</span><span class="k">this</span><span class="p">,</span><span class="w"> </span><span class="s2">"bernat.ch"</span><span class="p">,</span><span class="w"> </span><span class="nx">providers</span><span class="p">.</span><span class="nx">aws</span><span class="p">)</span>
<span class="w">  </span><span class="p">.</span><span class="nx">sign</span><span class="p">(</span><span class="nx">dnsCMK</span><span class="p">)</span>
<span class="w">  </span><span class="p">.</span><span class="nx">registrar</span><span class="p">(</span><span class="nx">providers</span><span class="p">.</span><span class="nx">gandiVB</span><span class="p">)</span>
<span class="w">  </span><span class="p">.</span><span class="nx">www</span><span class="p">(</span><span class="s2">"@"</span><span class="p">,</span><span class="w"> </span><span class="nx">servers</span><span class="p">)</span>
<span class="w">  </span><span class="p">.</span><span class="nx">www</span><span class="p">(</span><span class="s2">"vincent"</span><span class="p">,</span><span class="w"> </span><span class="nx">servers</span><span class="p">)</span>
<span class="w">  </span><span class="p">.</span><span class="nx">www</span><span class="p">(</span><span class="s2">"media"</span><span class="p">,</span><span class="w"> </span><span class="nx">servers</span><span class="p">)</span>
<span class="w">  </span><span class="p">.</span><span class="nx">fastmailMX</span><span class="p">([</span><span class="s2">"vincent"</span><span class="p">]);</span>
</code></pre></div>

<p>Toute la magie est située dans les fonctions appelées. Vous pouvez regarder le
fichier <a href="https://github.com/vincentbernat/cdktf-take1/blob/51dfb3f4dbeb/luffy/dns.ts">dns.ts</a> dans le dépôt <a href="https://github.com/vincentbernat/cdktf-take1/tree/51dfb3f4dbeb">cdktf-take1</a> pour comprendre comment cela
fonctionne. Rapidement :</p>
<ul>
<li><code>Route53Zone()</code> crée une zone sur <em>Route 53</em> ;</li>
<li><code>sign()</code> signe la zone avec une clé de signature ;</li>
<li><code>registrar()</code> inscrit la zone auprès du registre de domaines et configure DNSSEC ;</li>
<li><code>www()</code> crée les enregistrements <code>A</code> et <code>AAAA</code> pour les serveurs web ;</li>
<li><code>fastmailMX()</code> crée les enregistrements <code>MX</code> et d'autres enregistrements liés
  pour configurer <em>Fastmail</em> en tant que fournisseur de courriel.</li>
</ul>
<p>Voici le contenu de la fonction <code>fastmailMX()</code>. Elle génère quelques
enregistrements et retourne la zone en cours pour faciliter le chaînage :</p>
<div class="language-typescript codehilite"><pre><span/><code><span class="nx">fastmailMX</span><span class="p">(</span><span class="nx">subdomains?</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">[])</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="p">(</span><span class="nx">subdomains</span><span class="w"> </span><span class="o">??</span><span class="w"> </span><span class="p">[])</span>
<span class="w">    </span><span class="p">.</span><span class="nx">concat</span><span class="p">([</span><span class="s2">"@"</span><span class="p">,</span><span class="w"> </span><span class="s2">"*"</span><span class="p">])</span>
<span class="w">    </span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">subdomain</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span>
<span class="w">      </span><span class="k">this</span><span class="p">.</span><span class="nx">MX</span><span class="p">(</span><span class="nx">subdomain</span><span class="p">,</span><span class="w"> </span><span class="p">[</span>
<span class="w">        </span><span class="s2">"10 in1-smtp.messagingengine.com."</span><span class="p">,</span>
<span class="w">        </span><span class="s2">"20 in2-smtp.messagingengine.com."</span><span class="p">,</span>
<span class="w">      </span><span class="p">])</span>
<span class="w">    </span><span class="p">);</span>
<span class="w">  </span><span class="k">this</span><span class="p">.</span><span class="nx">TXT</span><span class="p">(</span><span class="s2">"@"</span><span class="p">,</span><span class="w"> </span><span class="s2">"v=spf1 include:spf.messagingengine.com ~all"</span><span class="p">);</span>
<span class="w">  </span><span class="p">[</span><span class="s2">"mesmtp"</span><span class="p">,</span><span class="w"> </span><span class="s2">"fm1"</span><span class="p">,</span><span class="w"> </span><span class="s2">"fm2"</span><span class="p">,</span><span class="w"> </span><span class="s2">"fm3"</span><span class="p">].</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">dk</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span>
<span class="w">    </span><span class="k">this</span><span class="p">.</span><span class="nx">CNAME</span><span class="p">(</span><span class="sb">`</span><span class="si">${</span><span class="nx">dk</span><span class="si">}</span><span class="sb">._domainkey`</span><span class="p">,</span><span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nx">dk</span><span class="si">}</span><span class="sb">.</span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">.dkim.fmhosted.com.`</span><span class="p">)</span>
<span class="w">  </span><span class="p">);</span>
<span class="w">  </span><span class="k">this</span><span class="p">.</span><span class="nx">TXT</span><span class="p">(</span><span class="s2">"_dmarc"</span><span class="p">,</span><span class="w"> </span><span class="s2">"v=DMARC1; p=none; sp=none"</span><span class="p">);</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="k">this</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<p>Je vous encourage à parcourir le <a href="https://github.com/vincentbernat/cdktf-take1/tree/51dfb3f4dbeb">dépôt</a> pour plus de détails !</p>
<h2 id="a-propos-de-pulumi">À propos de Pulumi</h2>
<p>Ma première tentative autour de <em>Terraform</em> a été d'utiliser <a href="https://www.pulumi.com/" title="Pulumi: universal infrastructure as code">Pulumi</a>. Vous
pouvez trouver cette tentative sur <a href="https://github.com/vincentbernat/pulumi-take1">GitHub</a>. C'est assez
similaire à ce que je fais actuellement avec <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em>. La principale différence
est que j'utilise Python au lieu de TypeScript<sup id="fnref:python"><a class="footnote-ref" href="#fn:python">2</a></sup> car ce dernier ne
m'était pas familier à l'époque.</p>
<p><em>Pulumi</em> est antérieur à <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em> et il utilise une approche légèrement
différente. <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em> génère une configuration pour <em>Terraform</em> (au format JSON au
lieu de <abbr title="HashiCorp Configuration Language">HCL</abbr>), laissant la planification, la gestion des états et le déploiement
à ce dernier. Il est donc lié aux limites de ce qui peut être exprimé par
<em>Terraform</em>, notamment lorsque vous devez transformer des données obtenues d'une
ressource à une autre<sup id="fnref:transform"><a class="footnote-ref" href="#fn:transform">3</a></sup>. <em>Pulumi</em> a besoin de fournisseurs spécifiques
pour chaque ressource. De nombreux fournisseurs encapsulent des fournisseurs
<em>Terraform</em>.</p>
<p>Bien que <em>Pulumi</em> offre une bonne expérience utilisateur, je suis passé à
<em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em> car écrire des fournisseurs pour <em>Pulumi</em> est une corvée. <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em> ne
nécessite pas de passer par cette étape. En dehors des grands acteurs (AWS,
Azure et Google Cloud), l'existence, la qualité et la fraîcheur des fournisseurs
<em>Pulumi</em> sont inégales. La plupart des fournisseurs s'appuient sur un
fournisseur <em>Terraform</em> et il se peut qu'ils soient en retard de quelques
versions, qu'il leur manque quelques ressources ou qu'ils présentent des bogues
qui leur sont propres.</p>
<p>Lorsqu'un fournisseur n'existe pas, vous pouvez en écrire un à l'aide de la
bibliothèque <a href="https://github.com/pulumi/pulumi-terraform-bridge">pulumi-terraform-bridge</a>. Le projet Pulumi fournit un
<a href="https://github.com/pulumi/pulumi-tf-provider-boilerplate">modèle</a> à cet effet. J'ai eu une mauvaise expérience avec celui-ci
lors de l'écriture de fournisseurs pour <a href="https://github.com/vincentbernat/pulumi-gandi-old" title="Deprecated Pulumi provider for Gandi">Gandi</a> et
<a href="https://github.com/vincentbernat/pulumi-vultr" title="Pulumi provider for Vultr">Vultr</a> : le <code>Makefile</code> <a href="https://github.com/pulumi/pulumi-tf-provider-boilerplate/pull/51" title="PR #51: Do not install pulumi automatically">installe automatiquement
Pulumi</a> en utilisant <code>curl | sh</code> et <a href="https://github.com/pulumi/pulumi-tf-provider-boilerplate/pull/52" title="PR #52: Fix Makefile to work with a POSIX shell">ne fonctionne pas avec
<code>/bin/sh</code></a>. Il y a un manque d'intérêt pour les contributions
communautaires<sup id="fnref:maint"><a class="footnote-ref" href="#fn:maint">4</a></sup> ou même pour <a href="https://twitter.com/briggsl/status/1473009153983623176">les fournisseurs pour les acteurs
tiers</a>.</p>
<h1 id="nixos-nixops">NixOS &amp; NixOps</h1>
<p><a href="https://nix.dev/manual/nix/stable/language/index.html" title="Nix, the language">Nix</a> est un langage de programmation purement fonctionnel.
<a href="https://nix.dev/manual/nix/stable/introduction.html" title="Nix, the package manager">Nix</a> est aussi le nom du gestionnaire de paquets qui est construit
au-dessus du langage <em>Nix</em>. Il permet aux utilisateurs d'installer des paquets
de manière déclarative. <a href="https://github.com/NixOS/nixpkgs">nixpkgs</a> est un dépôt de paquets. Vous pouvez
<a href="https://nix.dev/manual/nix/stable/installation/multi-user.html">installer <em>Nix</em></a> au-dessus d'une distribution Linux ordinaire. Si
vous voulez plus de détails, une bonne ressource est le <a href="https://nixos.org/">site
officiel</a>, notamment la <a href="https://nixos.org/learn/">section « Apprendre »</a>. La
courbe d'apprentissage est rude, mais la récompense est grande.</p>
<h2 id="nixos-distribution-linux-declarative">NixOS : distribution Linux déclarative</h2>
<p><a href="https://nixos.org/" title="Nix: reproducible builds and deployments">NixOS</a> est une distribution Linux construite au-dessus du gestionnaire de
paquets <em>Nix</em>. Voici un bout de configuration pour ajouter quelques paquets :</p>
<div class="language-nix codehilite"><pre><span/><code>environment<span class="o">.</span><span class="ss">systemPackages</span> <span class="o">=</span> <span class="k">with</span> pkgs<span class="p">;</span>
  <span class="p">[</span>
    bat
    htop
    liboping
    mg
    mtr
    ncdu
    tmux
  <span class="p">];</span>
</code></pre></div>

<p>Il est possible de modifier une dérivation<sup id="fnref:derivation"><a class="footnote-ref" href="#fn:derivation">5</a></sup> existante pour utiliser une version
différente, activer une fonctionnalité spécifique ou appliquer un correctif.
Voici comment j'active et configure <em>Nginx</em> pour désactiver le module <code>stream</code>,
ajouter le module de <a href="https://github.com/google/ngx_brotli">compression Brotli</a> et ajouter
le module d'<a href="https://github.com/masonicboom/ipscrub">anonymisation des adresses IP</a>. De
plus, au lieu d'utiliser <em>OpenSSL 3</em>, je continue à utiliser <em>OpenSSL
1.1</em><sup id="fnref:openssl"><a class="footnote-ref" href="#fn:openssl">6</a></sup>.</p>
<div class="language-nix codehilite"><pre><span/><code>services<span class="o">.</span><span class="ss">nginx</span> <span class="o">=</span> <span class="p">{</span>
  <span class="ss">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>

  <span class="ss">package</span> <span class="o">=</span> <span class="p">(</span>pkgs<span class="o">.</span>nginxStable<span class="o">.</span>override <span class="p">{</span>
    <span class="ss">withStream</span> <span class="o">=</span> <span class="no">false</span><span class="p">;</span>
    <span class="ss">modules</span> <span class="o">=</span> <span class="k">with</span> pkgs<span class="o">.</span>nginxModules<span class="p">;</span> <span class="p">[</span>
      brotli
      ipscrub
    <span class="p">];</span>
    <span class="ss">openssl</span> <span class="o">=</span> pkgs<span class="o">.</span>openssl_1_1<span class="p">;</span>
  <span class="p">});</span>
</code></pre></div>

<p>Si vous avez besoin d'ajouter certaines modifications, c'est également possible.
À titre d'exemple, voici comment j'ai corrigé en avance les <a href="https://github.com/Netflix/security-bulletins/blob/master/advisories/third-party/2019-002.md">failles de sécurité
découvertes en 2019 dans <em>Nginx</em></a> en attendant que cela soit corrigé
dans <em>NixOS</em><sup id="fnref:security"><a class="footnote-ref" href="#fn:security">7</a></sup> :</p>
<div class="language-nix codehilite"><pre><span/><code>services<span class="o">.</span>nginx<span class="o">.</span><span class="ss">package</span> <span class="o">=</span> pkgs<span class="o">.</span>nginxStable<span class="o">.</span>overrideAttrs <span class="p">(</span>old<span class="p">:</span> <span class="p">{</span>
  <span class="ss">patches</span> <span class="o">=</span> old<span class="o">.</span>patches <span class="o">++</span> <span class="p">[</span>
    <span class="c1"># HTTP/2: reject zero length headers with PROTOCOL_ERROR.</span>
    <span class="p">(</span>pkgs<span class="o">.</span>fetchpatch <span class="p">{</span>
      <span class="ss">url</span> <span class="o">=</span> <span class="l">https://github.com/nginx/nginx/commit/dbdd</span><span class="p">[</span><span class="err">…</span><span class="p">]</span><span class="o">.</span>patch<span class="p">;</span>
      <span class="ss">sha256</span> <span class="o">=</span> <span class="s2">"a48190[…]"</span><span class="p">;</span>
    <span class="p">})</span>
    <span class="c1"># HTTP/2: limited number of DATA frames.</span>
    <span class="p">(</span>pkgs<span class="o">.</span>fetchpatch <span class="p">{</span>
      <span class="ss">url</span> <span class="o">=</span> <span class="l">https://github.com/nginx/nginx/commit/94c5</span><span class="p">[</span><span class="err">…</span><span class="p">]</span><span class="o">.</span>patch<span class="p">;</span>
      <span class="ss">sha256</span> <span class="o">=</span> <span class="s2">"af591a[…]"</span><span class="p">;</span>
    <span class="p">})</span>
    <span class="c1">#  HTTP/2: limited number of PRIORITY frames.</span>
    <span class="p">(</span>pkgs<span class="o">.</span>fetchpatch <span class="p">{</span>
      <span class="ss">url</span> <span class="o">=</span> <span class="l">https://github.com/nginx/nginx/commit/39bb</span><span class="p">[</span><span class="err">…</span><span class="p">]</span><span class="o">.</span>patch<span class="p">;</span>
      <span class="ss">sha256</span> <span class="o">=</span> <span class="s2">"1ad8fe[…]"</span><span class="p">;</span>
    <span class="p">})</span>
  <span class="p">];</span>
<span class="p">});</span>
</code></pre></div>

<p>Si cela vous intéresse, jetez un coup d'œil à ma configuration relativement
réduite : <a href="https://github.com/vincentbernat/nixops-take1/blob/7e583aca7906/tags/common.nix"><code>common.nix</code></a> contient la configuration à appliquer sur
tous les serveurs (SSH, utilisateurs, paquets communs), <a href="https://github.com/vincentbernat/nixops-take1/blob/7e583aca7906/tags/web.nix"><code>web.nix</code></a>
contient la configuration pour les serveurs web et <a href="https://github.com/vincentbernat/nixops-take1/blob/7e583aca7906/tags/isso.nix"><code>isso.nix</code></a>
exécute <a href="/en/blog/2020-docker-nixos-isso" title="Running Isso on NixOS in a Docker container">Isso</a> dans un conteneur <em>systemd</em>.</p>
<h2 id="nixops-outil-de-deploiement-pour-nixos">NixOps : outil de déploiement pour NixOS</h2>
<p>Sur un seul nœud, la configuration de <em>NixOS</em> se trouve dans le fichier
<code>/etc/nixos/configuration.nix</code>. Après l'avoir modifiée, vous devez exécuter la
commande <code>nixos-rebuild switch</code>. <em>Nix</em> va chercher toutes les dépendances
possibles dans le cache binaire et construit le reste. Il crée une nouvelle
entrée dans le menu du chargeur de démarrage et active la nouvelle
configuration.</p>
<p>Pour gérer plusieurs nœuds, il existe plusieurs options, dont <a href="https://github.com/NixOS/nixops" title="NixOps:  tool for deploying to NixOS machines in a network or cloud">NixOps</a>,
<a href="https://github.com/serokell/deploy-rs" title="simple multi-profile Nix flake deploy tool">deploy-rs</a>, <a href="https://github.com/nix-community/colmena" title="Simple, stateless NixOS deployment tool">Colmena</a> et <a href="https://github.com/DBCDK/morph/" title="NixOS deployment tool">morph</a>. Je ne les connais pas toutes, mais de
mon point de vue, les différences ne sont pas si importantes. Il est également
possible de construire un tel outil soi-même car <em>Nix</em> fournit les blocs de
construction les plus importants : <code>nix build</code> et <code>nix copy</code>. <em>NixOps</em> est l'un
des premiers outils publiés mais je vous encourage à explorer les alternatives.</p>
<p>La configuration de <em>NixOps</em> est écrite avec le langage <em>Nix</em>. Voici une
configuration simplifiée pour déployer <code>znc01.luffy.cx</code>, <code>web01.luffy.cx</code> et
<code>web02.luffy.cx</code>, à l'aide des fonctions <code>server</code> et <code>web</code> :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="k">let</span>
  <span class="ss">server</span> <span class="o">=</span> hardware<span class="p">:</span> name<span class="p">:</span> imports<span class="p">:</span> <span class="p">{</span>
    deployment<span class="o">.</span><span class="ss">targetHost</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span>name<span class="si">}</span><span class="s2">.luffy.cx"</span><span class="p">;</span>
    networking<span class="o">.</span><span class="ss">hostName</span> <span class="o">=</span> name<span class="p">;</span>
    networking<span class="o">.</span><span class="ss">domain</span> <span class="o">=</span> <span class="s2">"luffy.cx"</span><span class="p">;</span>
    <span class="ss">imports</span> <span class="o">=</span> <span class="p">[</span> <span class="p">(</span><span class="l">./hardware/.</span> <span class="o">+</span> <span class="s2">"/</span><span class="si">${</span>hardware<span class="si">}</span><span class="s2">.nix"</span><span class="p">)</span> <span class="p">]</span> <span class="o">++</span> imports<span class="p">;</span>
  <span class="p">};</span>
  <span class="ss">web</span> <span class="o">=</span> hardware<span class="p">:</span> idx<span class="p">:</span> imports<span class="p">:</span>
    server hardware <span class="s2">"web</span><span class="si">${</span>lib<span class="o">.</span>fixedWidthNumber <span class="mi">2</span> idx<span class="si">}</span><span class="s2">"</span> <span class="p">([</span> <span class="l">./web.nix</span> <span class="p">]</span> <span class="o">++</span> imports<span class="p">);</span>
<span class="k">in</span> <span class="p">{</span>
  network<span class="o">.</span><span class="ss">description</span> <span class="o">=</span> <span class="s2">"Luffy infrastructure"</span><span class="p">;</span>
  network<span class="o">.</span><span class="ss">enableRollback</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
  <span class="ss">defaults</span> <span class="o">=</span> <span class="nb">import</span> <span class="l">./common.nix</span><span class="p">;</span>
  <span class="ss">znc01</span> <span class="o">=</span> server <span class="s2">"exoscale"</span> <span class="p">[</span> <span class="l">./znc.nix</span> <span class="p">];</span>
  <span class="ss">web01</span> <span class="o">=</span> web <span class="s2">"hetzner"</span> <span class="mi">1</span> <span class="p">[</span> <span class="l">./isso.nix</span> <span class="p">];</span>
  <span class="ss">web02</span> <span class="o">=</span> web <span class="s2">"hetzner"</span> <span class="mi">2</span> <span class="p">[];</span>
<span class="p">}</span>
</code></pre></div>

<h1 id="connecter-le-tout-avec-nix">Connecter le tout avec Nix</h1>
<p>L'écosystème <em>Nix</em> est une solution unifiée aux différents problèmes liés à la
gestion des logiciels et des configurations. Les <a href="https://web.archive.org/web/2022/https://nixos.org/guides/declarative-and-reproducible-developer-environments.html">environnements de
développement déclaratifs et reproductibles</a> en constituent une caractéristique très intéressante.
Cela ressemble aux environnements virtuels de Python, mais ils ne sont pas
spécifiques à un langage.</p>
<h2 id="courte-introduction-aux-flakes-nix">Courte introduction aux « flakes » Nix</h2>
<p>J'utilise les « <a href="https://wiki.nixos.org/wiki/Flakes" title="Flakes on NixOS wiki">flakes</a> », une nouvelle fonctionnalité de <em>Nix</em> qui améliore
la reproductibilité en fixant toutes les dépendances et en isolant le processus
de construction. Bien que cette fonctionnalité soit marquée comme
expérimentale<sup id="fnref:experimental"><a class="footnote-ref" href="#fn:experimental">8</a></sup>, elle est de plus en plus populaire et vous pouvez
trouver <code>flake.nix</code> et <code>flake.lock</code> à la racine de certains dépôts.</p>
<p>A titre d'exemple, voici le contenu du <code>flake.nix</code> livré avec <a href="/en/blog/2013-snimpy" title="Snimpy: SNMP &amp; Python">Snimpy</a>, un
outil SNMP interactif pour Python reposant sur <a href="https://www.ibr.cs.tu-bs.de/projects/libsmi/" title="libsmi: library to Access SMI MIB Information">libsmi</a>, une bibliothèque C :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="p">{</span>
  <span class="ss">inputs</span> <span class="o">=</span> <span class="p">{</span>
    nixpkgs<span class="o">.</span><span class="ss">url</span> <span class="o">=</span> <span class="s2">"nixpkgs"</span><span class="p">;</span>
    flake-utils<span class="o">.</span><span class="ss">url</span> <span class="o">=</span> <span class="s2">"github:numtide/flake-utils"</span><span class="p">;</span>
  <span class="p">};</span>
  <span class="ss">outputs</span> <span class="o">=</span> <span class="p">{</span> self<span class="p">,</span> <span class="o">...</span> <span class="p">}@</span>inputs<span class="p">:</span>
    inputs<span class="o">.</span>flake-utils<span class="o">.</span>lib<span class="o">.</span>eachDefaultSystem <span class="p">(</span>system<span class="p">:</span>
      <span class="k">let</span>
        <span class="ss">pkgs</span> <span class="o">=</span> inputs<span class="o">.</span>nixpkgs<span class="o">.</span>legacyPackages<span class="o">.</span><span class="s2">"</span><span class="si">${</span>system<span class="si">}</span><span class="s2">"</span><span class="p">;</span>
      <span class="k">in</span>
      <span class="p">{</span>
        <span class="c1"># nix build</span>
        packages<span class="o">.</span><span class="ss">default</span> <span class="o">=</span> pkgs<span class="o">.</span>python3Packages<span class="o">.</span>buildPythonPackage <span class="p">{</span>
          <span class="ss">name</span> <span class="o">=</span> <span class="s2">"snimpy"</span><span class="p">;</span>
          <span class="ss">src</span> <span class="o">=</span> self<span class="p">;</span>
          <span class="ss">preConfigure</span> <span class="o">=</span> <span class="s s-Multiline">''echo "1.0.0-0-000000000000" &gt; version.txt''</span><span class="p">;</span>
          <span class="ss">checkPhase</span> <span class="o">=</span> <span class="s2">"pytest"</span><span class="p">;</span>
          <span class="ss">checkInputs</span> <span class="o">=</span> <span class="k">with</span> pkgs<span class="o">.</span>python3Packages<span class="p">;</span> <span class="p">[</span> pytest mock coverage <span class="p">];</span>
          <span class="ss">propagatedBuildInputs</span> <span class="o">=</span> <span class="k">with</span> pkgs<span class="o">.</span>python3Packages<span class="p">;</span> <span class="p">[</span> cffi pysnmp ipython <span class="p">];</span>
          <span class="ss">buildInputs</span> <span class="o">=</span> <span class="p">[</span> pkgs<span class="o">.</span>libsmi <span class="p">];</span>
        <span class="p">};</span>
        <span class="c1"># nix run + nix shell</span>
        apps<span class="o">.</span><span class="ss">default</span> <span class="o">=</span> <span class="p">{</span> 
          <span class="ss">type</span> <span class="o">=</span> <span class="s2">"app"</span><span class="p">;</span>
          <span class="ss">program</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span>self<span class="o">.</span>packages<span class="o">.</span><span class="s2">"</span><span class="si">${</span>system<span class="si">}</span><span class="s2">"</span><span class="o">.</span>default<span class="si">}</span><span class="s2">/bin/snimpy"</span><span class="p">;</span>
        <span class="p">};</span>
        <span class="c1"># nix develop</span>
        devShells<span class="o">.</span><span class="ss">default</span> <span class="o">=</span> pkgs<span class="o">.</span>mkShell <span class="p">{</span>
          <span class="ss">name</span> <span class="o">=</span> <span class="s2">"snimpy-dev"</span><span class="p">;</span>
          <span class="ss">buildInputs</span> <span class="o">=</span> <span class="p">[</span>
            self<span class="o">.</span>packages<span class="o">.</span><span class="s2">"</span><span class="si">${</span>system<span class="si">}</span><span class="s2">"</span><span class="o">.</span>default<span class="o">.</span>inputDerivation
            pkgs<span class="o">.</span>python3Packages<span class="o">.</span>ipython
          <span class="p">];</span>
        <span class="p">};</span>
      <span class="p">});</span>
<span class="p">}</span>
</code></pre></div>

<p>Si <em>Nix</em> est installé sur votre système :</p>
<ul>
<li><code>nix run github:vincentbernat/snimpy</code> exécute <em>Snimpy</em> ;</li>
<li><code>nix shell github:vincentbernat/snimpy</code> fournit un shell avec <em>Snimpy</em> prêt à être utilisé ;</li>
<li><code>nix build github:vincentbernat/snimpy</code> construit le paquet Python ;</li>
<li><code>nix develop .</code> fournit un shell pour développer autour de <em>Snimpy</em> depuis un
  clône du dépôt<sup id="fnref:checkout"><a class="footnote-ref" href="#fn:checkout">9</a></sup>.</li>
</ul>
<p>Pour plus d'informations sur les <em>flakes</em>, regardez le <a href="https://www.tweag.io/blog/2020-05-25-flakes/" title="Nix Flakes, Part 1: An introduction and tutorial">tutoriel de
Tweag</a>.</p>
<h2 id="nix-et-cdktf">Nix et <abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></h2>
<p>A la racine du <a href="https://github.com/vincentbernat/cdktf-take1/tree/51dfb3f4dbeb">dépôt que j'utilise pour <abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></a>, il y a un fichier
<a href="https://github.com/vincentbernat/cdktf-take1/blob/51dfb3f4dbeb/flake.nix"><code>flake.nix</code></a> pour configurer un shell avec <em>Terraform</em>
et <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em> installés et avec les variables d'environnement nécessaires pour
automatiser mon infrastructure.</p>
<p>Terraform est déjà présent dans <em>nixpkgs</em>, mais je dois appliquer une rustine sur
le fournisseur <em>Gandi</em>. Ce n'est pas un problème avec Nix !</p>
<div class="language-nix codehilite"><pre><span/><code><span class="ss">terraform</span> <span class="o">=</span> pkgs<span class="o">.</span>terraform<span class="o">.</span>withPlugins <span class="p">(</span>p<span class="p">:</span> <span class="p">[</span>
  p<span class="o">.</span>aws
  p<span class="o">.</span>hcloud
  p<span class="o">.</span>vultr
  <span class="p">(</span>p<span class="o">.</span>gandi<span class="o">.</span>overrideAttrs
    <span class="p">(</span>old<span class="p">:</span> <span class="p">{</span>
      <span class="ss">src</span> <span class="o">=</span> pkgs<span class="o">.</span>fetchFromGitHub <span class="p">{</span>
        <span class="ss">owner</span> <span class="o">=</span> <span class="s2">"vincentbernat"</span><span class="p">;</span>
        <span class="ss">repo</span> <span class="o">=</span> <span class="s2">"terraform-provider-gandi"</span><span class="p">;</span>
        <span class="ss">rev</span> <span class="o">=</span> <span class="s2">"feature/livedns-key"</span><span class="p">;</span>
        <span class="ss">hash</span> <span class="o">=</span> <span class="s2">"sha256-V16BIjo5/rloQ1xTQrdd0snoq1OPuDh3fQNW7kiv/kQ="</span><span class="p">;</span>
      <span class="p">};</span>
    <span class="p">}))</span>
<span class="p">]);</span>
</code></pre></div>

<p><em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em> est écrit en TypeScript. J'ai un fichier
<a href="https://github.com/vincentbernat/cdktf-take1/blob/51dfb3f4dbeb/package.json"><code>package.json</code></a> avec toutes les dépendances
nécessaires, y compris celles pour utiliser TypeScript comme langage cible :</p>
<div class="language-json codehilite"><pre><span/><code><span class="p">{</span>
<span class="w">  </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cdktf-take1"</span><span class="p">,</span>
<span class="w">  </span><span class="nt">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span>
<span class="w">  </span><span class="nt">"main"</span><span class="p">:</span><span class="w"> </span><span class="s2">"main.js"</span><span class="p">,</span>
<span class="w">  </span><span class="nt">"types"</span><span class="p">:</span><span class="w"> </span><span class="s2">"main.ts"</span><span class="p">,</span>
<span class="w">  </span><span class="nt">"private"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w">  </span><span class="nt">"dependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nt">"@types/node"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^14.18.30"</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"cdktf"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^0.13.3"</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"cdktf-cli"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^0.13.3"</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"constructs"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^10.1.151"</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"eslint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^8.27.0"</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"prettier"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^2.7.1"</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"ts-node"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^10.9.1"</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"typescript"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^3.9.10"</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"typescript-language-server"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^2.1.0"</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p>J'utilise <a href="https://yarnpkg.com/" title="Yarn package manager">Yarn</a> pour obtenir un fichier <a href="https://github.com/vincentbernat/cdktf-take1/blob/51dfb3f4dbeb/yarn.lock"><code>yarn.lock</code></a>
qui peut ensuite être utilisé directement pour construire la dérivation
contenant toutes les dépendances :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="ss">nodeEnv</span> <span class="o">=</span> pkgs<span class="o">.</span>mkYarnModules <span class="p">{</span>
  <span class="ss">pname</span> <span class="o">=</span> <span class="s2">"cdktf-take1-js-modules"</span><span class="p">;</span>
  <span class="ss">version</span> <span class="o">=</span> <span class="s2">"1.0.0"</span><span class="p">;</span>
  <span class="ss">packageJSON</span> <span class="o">=</span> <span class="l">./package.json</span><span class="p">;</span>
  <span class="ss">yarnLock</span> <span class="o">=</span> <span class="l">./yarn.lock</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div>

<p>L'étape suivant est de générer les fournisseurs <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em> à partir des
fournisseurs <em>Terraform</em> et de les inclure dans une dérivation :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="ss">cdktfProviders</span> <span class="o">=</span> pkgs<span class="o">.</span>stdenvNoCC<span class="o">.</span>mkDerivation <span class="p">{</span>
  <span class="ss">name</span> <span class="o">=</span> <span class="s2">"cdktf-providers"</span><span class="p">;</span>
  <span class="ss">nativeBuildInputs</span> <span class="o">=</span> <span class="p">[</span>
    pkgs<span class="o">.</span>nodejs
    terraform
  <span class="p">];</span>
  <span class="ss">src</span> <span class="o">=</span> nix-filter <span class="p">{</span>
    <span class="ss">root</span> <span class="o">=</span> <span class="l">./.</span><span class="p">;</span>
    <span class="ss">include</span> <span class="o">=</span> <span class="p">[</span> <span class="l">./cdktf.json</span> <span class="l">./tsconfig.json</span> <span class="p">];</span>
  <span class="p">};</span>
  <span class="ss">buildPhase</span> <span class="o">=</span> <span class="s s-Multiline">''</span>
<span class="s s-Multiline">    export HOME=$(mktemp -d)</span>
<span class="s s-Multiline">    export CHECKPOINT_DISABLE=1</span>
<span class="s s-Multiline">    export DISABLE_VERSION_CHECK=1</span>
<span class="s s-Multiline">    export PATH=</span><span class="si">${</span>nodeEnv<span class="si">}</span><span class="s s-Multiline">/node_modules/.bin:$PATH</span>
<span class="s s-Multiline">    ln -nsf </span><span class="si">${</span>nodeEnv<span class="si">}</span><span class="s s-Multiline">/node_modules node_modules</span>

<span class="s s-Multiline">    # Build all providers we have in terraform</span>
<span class="s s-Multiline">    for provider in $(cd </span><span class="si">${</span>terraform<span class="si">}</span><span class="s s-Multiline">/libexec/terraform-providers; echo */*/*/*); do</span>
<span class="s s-Multiline">      version=</span><span class="se">''$</span><span class="s s-Multiline">{provider##*/}</span>
<span class="s s-Multiline">      provider=</span><span class="se">''$</span><span class="s s-Multiline">{provider%/*}</span>
<span class="s s-Multiline">      echo "Build $provider@$version"</span>
<span class="s s-Multiline">      cdktf provider add --force-local $provider@$version | cat</span>
<span class="s s-Multiline">    done</span>
<span class="s s-Multiline">    echo "Compile TS → JS"</span>
<span class="s s-Multiline">    tsc</span>
<span class="s s-Multiline">  ''</span><span class="p">;</span>
  <span class="ss">installPhase</span> <span class="o">=</span> <span class="s s-Multiline">''</span>
<span class="s s-Multiline">    mv .gen $out</span>
<span class="s s-Multiline">    ln -nsf </span><span class="si">${</span>nodeEnv<span class="si">}</span><span class="s s-Multiline">/node_modules $out/node_modules</span>
<span class="s s-Multiline">  ''</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div>

<p>Enfin, nous définissons l'environnement de développement :</p>
<div class="language-nix codehilite"><pre><span/><code>devShells<span class="o">.</span><span class="ss">default</span> <span class="o">=</span> pkgs<span class="o">.</span>mkShell <span class="p">{</span>
  <span class="ss">name</span> <span class="o">=</span> <span class="s2">"cdktf-take1"</span><span class="p">;</span>
  <span class="ss">buildInputs</span> <span class="o">=</span> <span class="p">[</span>
    pkgs<span class="o">.</span>nodejs
    pkgs<span class="o">.</span>yarn
    terraform
  <span class="p">];</span>
  <span class="ss">shellHook</span> <span class="o">=</span> <span class="s s-Multiline">''</span>
<span class="s s-Multiline">    # No telemetry</span>
<span class="s s-Multiline">    export CHECKPOINT_DISABLE=1</span>
<span class="s s-Multiline">    # No autoinstall of plugins</span>
<span class="s s-Multiline">    export CDKTF_DISABLE_PLUGIN_CACHE_ENV=1</span>
<span class="s s-Multiline">    # Do not check version</span>
<span class="s s-Multiline">    export DISABLE_VERSION_CHECK=1</span>
<span class="s s-Multiline">    # Access to node modules</span>
<span class="s s-Multiline">    export PATH=$PWD/node_modules/.bin:$PATH</span>
<span class="s s-Multiline">    ln -nsf </span><span class="si">${</span>nodeEnv<span class="si">}</span><span class="s s-Multiline">/node_modules node_modules</span>
<span class="s s-Multiline">    ln -nsf </span><span class="si">${</span>cdktfProviders<span class="si">}</span><span class="s s-Multiline"> .gen</span>

<span class="s s-Multiline">    # Credentials</span>
<span class="s s-Multiline">    for p in \</span>
<span class="s s-Multiline">      njf.nznmba.pbz/Nqzvavfgengbe \</span>
<span class="s s-Multiline">      urgmare.pbz/ivaprag@oreang.pu \</span>
<span class="s s-Multiline">      ihyge.pbz/ihyge@ivaprag.oreang.pu; do</span>
<span class="s s-Multiline">        eval $(pass show $(echo $p | tr 'A-Za-z' 'N-ZA-Mn-za-m') | grep '^export')</span>
<span class="s s-Multiline">    done</span>
<span class="s s-Multiline">    eval $(pass show personal/cdktf/secrets | grep '^export')</span>
<span class="s s-Multiline">    export TF_VAR_hcloudToken="$HCLOUD_TOKEN"</span>
<span class="s s-Multiline">    export TF_VAR_vultrApiKey="$VULTR_API_KEY"</span>
<span class="s s-Multiline">    unset VULTR_API_KEY HCLOUD_TOKEN</span>
<span class="s s-Multiline">  ''</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div>

<p>Les dérivations listées dans <code>buildInputs</code> sont disponibles dans le shell
fourni. Le contenu de <code>shellHook</code> est exécuté lors du démarrage du shell. Il
établit des liens symboliques pour rendre disponible l'environnement JavaScript
construit à une étape précédente, ainsi que les fournisseurs <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em> générés. Il
exporte également toutes les informations d'identification<sup id="fnref:obfuscated"><a class="footnote-ref" href="#fn:obfuscated">10</a></sup>.</p>
<p>J'utilise également <a href="https://github.com/direnv/direnv/" title="direnv: unclutter your .profile">direnv</a> avec un fichier <a href="https://github.com/vincentbernat/cdktf-take1/blob/51dfb3f4dbeb/.envrc"><code>.envrc</code></a>
pour passer automatiquement dans l'environnement de développement. Cela permet
également à ce dernier d'être disponible depuis <em>Emacs</em>, notamment lors de
l'utilisation de <a href="https://github.com/emacs-lsp/lsp-mode/" title="Language Server Protocol Support for Emacs">lsp-mode</a> pour obtenir les complétions. <code>nix develop .</code>
permet aussi d'activer manuellement l'environnement.</p>
<p>J'utilise les commandes suivantes pour déployer<sup id="fnref:cdktf-cli"><a class="footnote-ref" href="#fn:cdktf-cli">11</a></sup> :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>cdktf<span class="w"> </span>synth
<span class="gp">$ </span><span class="nb">cd</span><span class="w"> </span>cdktf.out/stacks/cdktf-take1
<span class="gp">$ </span>terraform<span class="w"> </span>plan<span class="w"> </span>--out<span class="w"> </span>plan
<span class="gp">$ </span>terraform<span class="w"> </span>apply<span class="w"> </span>plan
<span class="gp">$ </span>terraform<span class="w"> </span>output<span class="w"> </span>-json<span class="w"> </span>&gt;<span class="w"> </span>~-automation/nixops-take1/cdktf.json
</code></pre></div>

<p>La dernière commande produit un fichier JSON contenant les données nécessaires
pour finir le déploiement avec <em>NixOps</em>.</p>
<h2 id="nixops">NixOps</h2>
<p>Le <a href="https://github.com/vincentbernat/nixops-take1/blob/7e583aca7906/cdktf.json">fichier JSON</a> exporté par <em>Terraform</em> contient la
liste des serveurs avec quelques attributs :</p>
<div class="language-json codehilite"><pre><span/><code><span class="p">{</span>
<span class="w">  </span><span class="nt">"hardware"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hetzner"</span><span class="p">,</span>
<span class="w">  </span><span class="nt">"ipv4Address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"5.161.44.145"</span><span class="p">,</span>
<span class="w">  </span><span class="nt">"ipv6Address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2a01:4ff:f0:b91::1"</span><span class="p">,</span>
<span class="w">  </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"web05.luffy.cx"</span><span class="p">,</span>
<span class="w">  </span><span class="nt">"tags"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w">    </span><span class="s2">"web"</span><span class="p">,</span>
<span class="w">    </span><span class="s2">"continent:NA"</span><span class="p">,</span>
<span class="w">    </span><span class="s2">"continent:SA"</span>
<span class="w">  </span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div>

<p>Dans le fichier <a href="https://github.com/vincentbernat/nixops-take1/blob/7e583aca7906/network.nix"><code>network.nix</code></a>, cette liste est
importée et transformée en un ensemble d'attributs décrivant les serveurs. Une
version simplifiée ressemble à cela :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="k">let</span>
  <span class="ss">lib</span> <span class="o">=</span> inputs<span class="o">.</span>nixpkgs<span class="o">.</span>lib<span class="p">;</span>
  <span class="ss">shortName</span> <span class="o">=</span> name<span class="p">:</span> <span class="nb">builtins</span><span class="o">.</span>elemAt <span class="p">(</span>lib<span class="o">.</span>splitString <span class="s2">"."</span> name<span class="p">)</span> <span class="mi">0</span><span class="p">;</span>
  <span class="ss">domainName</span> <span class="o">=</span> name<span class="p">:</span> lib<span class="o">.</span>concatStringsSep <span class="s2">"."</span> <span class="p">(</span><span class="nb">builtins</span><span class="o">.</span>tail <span class="p">(</span>lib<span class="o">.</span>splitString <span class="s2">"."</span> name<span class="p">));</span>
  <span class="ss">server</span> <span class="o">=</span> hardware<span class="p">:</span> name<span class="p">:</span> imports<span class="p">:</span> <span class="p">{</span>
    <span class="ss">networking</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">hostName</span> <span class="o">=</span> shortName name<span class="p">;</span>
      <span class="ss">domain</span> <span class="o">=</span> domainName name<span class="p">;</span>
    <span class="p">};</span>
    deployment<span class="o">.</span><span class="ss">targetHost</span> <span class="o">=</span> name<span class="p">;</span>
    <span class="ss">imports</span> <span class="o">=</span> <span class="p">[</span> <span class="p">(</span><span class="l">./hardware/.</span> <span class="o">+</span> <span class="s2">"/</span><span class="si">${</span>hardware<span class="si">}</span><span class="s2">.nix"</span><span class="p">)</span> <span class="p">]</span> <span class="o">++</span> imports<span class="p">;</span>
  <span class="p">};</span>
  <span class="ss">cdktf-servers-json</span> <span class="o">=</span> <span class="p">(</span>lib<span class="o">.</span>importJSON <span class="l">./cdktf.json</span><span class="p">)</span><span class="o">.</span>servers<span class="o">.</span>value<span class="p">;</span>
  <span class="ss">cdktf-servers</span> <span class="o">=</span> <span class="nb">map</span>
    <span class="p">(</span>s<span class="p">:</span>
      <span class="k">let</span>
        <span class="ss">tags-maybe-import</span> <span class="o">=</span> <span class="nb">map</span> <span class="p">(</span>t<span class="p">:</span> <span class="l">./.</span> <span class="o">+</span> <span class="s2">"/</span><span class="si">${</span>t<span class="si">}</span><span class="s2">.nix"</span><span class="p">)</span> s<span class="o">.</span>tags<span class="p">;</span>
        <span class="ss">tags-import</span> <span class="o">=</span> <span class="nb">builtins</span><span class="o">.</span>filter <span class="p">(</span>t<span class="p">:</span> <span class="nb">builtins</span><span class="o">.</span>pathExists t<span class="p">)</span> tags-maybe-import<span class="p">;</span>
      <span class="k">in</span>
      <span class="p">{</span>
        <span class="ss">name</span> <span class="o">=</span> shortName s<span class="o">.</span>name<span class="p">;</span>
        <span class="ss">value</span> <span class="o">=</span> server s<span class="o">.</span>hardware s<span class="o">.</span>name tags-import<span class="p">;</span>
      <span class="p">})</span>
    cdktf-servers-json<span class="p">;</span>
<span class="k">in</span>
<span class="p">{</span>
  <span class="o">//</span> <span class="p">[</span><span class="err">…</span><span class="p">]</span>
<span class="p">}</span> <span class="o">//</span> <span class="nb">builtins</span><span class="o">.</span>listToAttrs cdktf-servers
</code></pre></div>

<p>Pour <code>web05</code>, on obtient ceci :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="ss">web05</span> <span class="o">=</span> <span class="p">{</span>
  <span class="ss">networking</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">hostName</span> <span class="o">=</span> <span class="s2">"web05"</span><span class="p">;</span>
    <span class="ss">domainName</span> <span class="o">=</span> <span class="s2">"luffy.cx"</span><span class="p">;</span>
  <span class="p">};</span>
  deployment<span class="o">.</span><span class="ss">targetHost</span> <span class="o">=</span> <span class="s2">"web05.luffy.cx"</span><span class="p">;</span>
  <span class="ss">imports</span> <span class="o">=</span> <span class="p">[</span> <span class="l">./hardware/hetzner.nix</span> <span class="l">./web.nix</span> <span class="p">];</span>
<span class="p">};</span>
</code></pre></div>

<p>Comme pour <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em>, à la racine du <a href="https://github.com/vincentbernat/nixops-take1/tree/7e583aca7906">dépôt que j'utilise pour
NixOps</a>, il y a un fichier <a href="https://github.com/vincentbernat/nixops-take1/blob/7e583aca7906/flake.nix"><code>flake.nix</code></a>
pour fournir un shell avec <em>NixOps</em> configuré. Comme <em>NixOps</em> ne supporte pas
les déploiements progressifs, j'utilise généralement ces commandes pour déployer
sur un unique serveur<sup id="fnref:disable"><a class="footnote-ref" href="#fn:disable">12</a></sup> :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>nix<span class="w"> </span>flake<span class="w"> </span>update
<span class="gp">$ </span>nixops<span class="w"> </span>deploy<span class="w"> </span>--include<span class="o">=</span>web04
<span class="gp">$ </span>./tests<span class="w"> </span>web04.luffy.cx
</code></pre></div>

<p>Si les tests se déroulent sans soucis, je déploie les autres nœuds un par un
avec la commande suivante :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span><span class="o">(</span><span class="nb">set</span><span class="w"> </span>-e<span class="p">;</span><span class="w"> </span><span class="k">for</span><span class="w"> </span>h<span class="w"> </span><span class="k">in</span><span class="w"> </span>web<span class="o">{</span><span class="m">03</span>..06<span class="o">}</span><span class="p">;</span><span class="w"> </span><span class="k">do</span><span class="w"> </span>nixops<span class="w"> </span>deploy<span class="w"> </span>--include<span class="o">=</span><span class="nv">$h</span><span class="p">;</span><span class="w"> </span><span class="k">done</span><span class="o">)</span>
</code></pre></div>

<p>La commande <code>nixops deploy</code> déploie tous les serveurs en parallèle et peut donc
provoquer une panne si tous les serveurs <em>Nginx</em> sont indisponibles au même
moment.</p>
<hr/>
<p>Cet article est en chantier depuis trois ans. Le contenu a été mis à jour et
affiné au fur et à mesure de mes expérimentations. Il y a encore beaucoup à
explorer<sup id="fnref:todo"><a class="footnote-ref" href="#fn:todo">13</a></sup>, mais j'estime que le contenu est désormais suffisant pour être
publié ! 🎄</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:hetzner">
<p>C'était un AMD Athlon 64 X2 5600+ avec 2 Go de RAM et 2 disques de
400 Go en RAID logiciel. Je payais quelque chose autour de 59 € par mois
pour cela. Si c'était une bonne affaire en 2008, en 2018, ce n'était plus
rentable. Il fonctionnait sous Debian Wheezy avec <a href="https://web.archive.org/web/2022/http://linux-vserver.org/Welcome_to_Linux-VServer.org" title="Linux-VServer">Linux-VServer</a> pour
l'isolation, tous deux dépassés en 2018. <a class="footnote-backref" href="#fnref:hetzner" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:python">
<p>Je n'ai pas non plus utilisé Python car le support de <em>Poetry</em> dans
<em>Nix</em> était <a href="https://github.com/nix-community/poetry2nix/issues/750" title="Issue #750: infinite recursion">cassé</a> quand j'ai commencé à essayer d'utiliser
<em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em>. <a class="footnote-backref" href="#fnref:python" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:transform">
<p><em>Pulumi</em> peut <a href="https://www.pulumi.com/docs/iac/concepts/inputs-outputs/apply/">appliquer des fonctions arbitraires</a> à l'aide de <code>apply()</code>. Cela permet de transformer les
données qui ne sont pas connues lors de l'étape de planification.
<em>Terraform</em> a des <a href="https://developer.hashicorp.com/terraform/language/functions">fonctions</a> pour un usage similaire mais
elles sont plus limitées. <a class="footnote-backref" href="#fnref:transform" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:maint">
<p>Les deux modifications mentionnées ne sont pas encore fusionnées. La
seconde est remplacée par la <a href="https://github.com/pulumi/pulumi-tf-provider-boilerplate/pull/61" title="PR #61: Explicitly specify bash as shell in Makefile">PR #61</a>, soumise deux mois plus
tard, qui impose l'utilisation de <code>/bin/bash</code>. J'ai également soumis la
<a href="https://github.com/pulumi/pulumi-tf-provider-boilerplate/pull/56" title="PR #56: Use go:embed instead of go generate for schema">PR #56</a>, qui a été fusionnée 4 mois plus tard et rapidement
annulée sans explication. <a class="footnote-backref" href="#fnref:maint" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:derivation">
<p>Grosso modo, une dérivation est un synonyme pour paquet dans
l'écosystème <em>Nix</em>. <a class="footnote-backref" href="#fnref:derivation" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:openssl">
<p><em>OpenSSL 3</em> a de nombreux <a href="https://github.com/openssl/openssl/issues/17627#issuecomment-1060123659">problèmes de performance</a>. <a class="footnote-backref" href="#fnref:openssl" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:security">
<p><em>NixOS</em> peut être un peu lent à intégrer les correctifs car il est
nécessaire de reconstruire une partie du cache binaire. Dans ce cas précis,
cela a été rapide : la vulnérabilité et les correctifs ont été publiés le 13
août 2019 et disponibles dans NixOS le 15 août. À titre de comparaison,
Debian n'a publié la version corrigée que le 22 août, ce qui est
inhabituellement tardif. <a class="footnote-backref" href="#fnref:security" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:experimental">
<p>Comme les <em>flakes</em> sont expérimentaux, de nombreuses
documentations ne les utilisent pas et c'est un aspect supplémentaire à
apprendre. <a class="footnote-backref" href="#fnref:experimental" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:checkout">
<p>Comme dans les autres exemples, il est possible de remplacer <code>.</code>
par <code>github:vincentbernat/snimpy</code>. Cependant, obtenir les dépendances de
<em>Snimpy</em> sans son code source a peu d'intérêt. <a class="footnote-backref" href="#fnref:checkout" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:obfuscated">
<p>J'utilise le gestionnaire de mots de passe <a href="https://www.passwordstore.org/" title="The standard Unix password manager">pass</a>. Le nom des
mots de passe sont brouillés uniquement pour éviter les courriers
indésirables. <a class="footnote-backref" href="#fnref:obfuscated" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
<li id="fn:cdktf-cli">
<p>La commande <code>cdktf</code> sait appeler les commandes <code>terraform</code>, mais
je préfère les utiliser directement car elles sont plus flexibles. <a class="footnote-backref" href="#fnref:cdktf-cli" title="Jump back to footnote 11 in the text">↩</a></p>
</li>
<li id="fn:disable">
<p>Si le changement est risqué, je <a href="https://github.com/vincentbernat/cdktf-take1/commit/606e6fb657179408b163664dd74ff5ab38b28246">désactive le serveur</a> avec <em><abbr title="Cloud Development Kit for Terraform">CDKTF</abbr></em>. Cela le retire des enregistrements DNS. <a class="footnote-backref" href="#fnref:disable" title="Jump back to footnote 12 in the text">↩</a></p>
</li>
<li id="fn:todo">
<p>Je voudrais remplacer <em>NixOps</em> avec une alternative gérant les
déploiements progressifs et les tests. Je voudrais aussi passer à <a href="https://developer.hashicorp.com/nomad" title="Nomad: Orchestration Made Easy">Nomad</a>
ou <em>Kubernetes</em> pour déployer les applications. <a class="footnote-backref" href="#fnref:todo" title="Jump back to footnote 13 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Déployer Isso sur NixOS dans un conteneur Docker</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2020-docker-nixos-isso" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2020-docker-nixos-isso#isso-thread" rel="replies" type="text/html"/>
    <updated>2020-11-01T20:55:49Z</updated>
    <id>http://www.luffy.cx/fr/blog/2020-docker-nixos-isso.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>Ce court article documente la façon dont je fais tourner <a href="https://isso-comments.de/" title="Isso: a commenting server similar to Disqus">Isso</a>, le
système de commentaires utilisé par ce blog, à l'intérieur d'un
conteneur Docker sur <a href="https://nixos.org/manual/nixpkgs/stable/#ssec-pkgs-dockerTools-buildImage">NixOS</a>, une distribution Linux construite sur
Nix. Nix est un gestionnaire de paquets déclaratif pour Linux et
d'autres systèmes Unix.</p>
<hr/>
<p><del>Bien que NixOS 20.09 intègre une <a href="https://github.com/NixOS/nixpkgs/blob/release-20.09/pkgs/servers/isso/default.nix">dérivation pour Isso</a>, elle est malheureusement cassée et repose sur Python 2.</del> Comme
j'utilise également un fork d'Isso, j'ai construit ma propre
dérivation, fortement inspirée de celle dans <a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/is/isso/package.nix">master</a><sup id="fnref:alternative"><a class="footnote-ref" href="#fn:alternative">1</a></sup> :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="ss">issoPackage</span> <span class="o">=</span> <span class="k">with</span> pkgs<span class="o">.</span>python3Packages<span class="p">;</span> buildPythonPackage <span class="k">rec</span> <span class="p">{</span>
  <span class="ss">pname</span> <span class="o">=</span> <span class="s2">"isso"</span><span class="p">;</span>
  <span class="ss">version</span> <span class="o">=</span> <span class="s2">"custom"</span><span class="p">;</span>

  <span class="ss">src</span> <span class="o">=</span> pkgs<span class="o">.</span>fetchFromGitHub <span class="p">{</span>
    <span class="c1"># Use my fork</span>
    <span class="ss">owner</span> <span class="o">=</span> <span class="s2">"vincentbernat"</span><span class="p">;</span>
    <span class="ss">repo</span> <span class="o">=</span> pname<span class="p">;</span>
    <span class="ss">rev</span> <span class="o">=</span> <span class="s2">"vbe/master"</span><span class="p">;</span>
    <span class="ss">sha256</span> <span class="o">=</span> <span class="s2">"0vkkvjcvcjcdzdj73qig32hqgjly8n3ln2djzmhshc04i6g9z07j"</span><span class="p">;</span>
  <span class="p">};</span>

  <span class="ss">propagatedBuildInputs</span> <span class="o">=</span> <span class="p">[</span>
    itsdangerous
    jinja2
    misaka
    html5lib
    werkzeug
    bleach
    flask-caching
  <span class="p">];</span>

  <span class="ss">buildInputs</span> <span class="o">=</span> <span class="p">[</span>
    cffi
  <span class="p">];</span>

  <span class="ss">checkInputs</span> <span class="o">=</span> <span class="p">[</span> nose <span class="p">];</span>

  <span class="ss">checkPhase</span> <span class="o">=</span> <span class="s s-Multiline">''</span>
<span class="s s-Multiline">    </span><span class="si">${</span>python<span class="o">.</span>interpreter<span class="si">}</span><span class="s s-Multiline"> setup.py nosetests</span>
<span class="s s-Multiline">  ''</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div>

<p>Je veux faire tourner Isso via <a href="https://gunicorn.org/" title="Gunicorn 'Green Unicorn': a Python WSGI HTTP Server for UNIX">Gunicorn</a>. À cet effet, je construis
un environnement Python combinant Isso et Gunicorn. Je peux alors
invoquer ce dernier avec <code>"${issoEnv}/bin/gunicorn"</code> à la manière d'un
<a href="https://docs.python.org/3/tutorial/venv.html" title="Documentation de Python : Virtual Environments and Packages">environnement Python virtuel</a></p>
<div class="language-nix codehilite"><pre><span/><code><span class="ss">issoEnv</span> <span class="o">=</span> pkgs<span class="o">.</span>python3<span class="o">.</span>buildEnv<span class="o">.</span>override <span class="p">{</span>
    <span class="ss">extraLibs</span> <span class="o">=</span> <span class="p">[</span>
      issoPackage
      pkgs<span class="o">.</span>python3Packages<span class="o">.</span>gunicorn
      pkgs<span class="o">.</span>python3Packages<span class="o">.</span>gevent
    <span class="p">];</span>
<span class="p">};</span>
</code></pre></div>

<p>Avant de construire une image Docker, je dois aussi définir le fichier
de configuration d'Isso :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="ss">issoConfig</span> <span class="o">=</span> pkgs<span class="o">.</span>writeText <span class="s2">"isso.conf"</span> <span class="s s-Multiline">''</span>
<span class="s s-Multiline">  [general]</span>
<span class="s s-Multiline">  dbpath = /db/comments.db</span>
<span class="s s-Multiline">  host =</span>
<span class="s s-Multiline">    https://vincent.bernat.ch</span>
<span class="s s-Multiline">    http://localhost:8080</span>
<span class="s s-Multiline">  notify = smtp</span>
<span class="s s-Multiline">  […]</span>
<span class="s s-Multiline">''</span><span class="p">;</span>
</code></pre></div>

<p>NixOS inclut un <a href="https://nixos.org/manual/nixpkgs/stable/#ssec-pkgs-dockerTools-buildImage">outil pratique permettant de construire une image
Docker</a> sans utiliser de
<code>Dockerfile</code> :</p>
<div class="language-nix codehilite"><pre><span/><code><span class="ss">issoDockerImage</span> <span class="o">=</span> pkgs<span class="o">.</span>dockerTools<span class="o">.</span>buildImage <span class="p">{</span>
  <span class="ss">name</span> <span class="o">=</span> <span class="s2">"isso"</span><span class="p">;</span>
  <span class="ss">tag</span> <span class="o">=</span> <span class="s2">"latest"</span><span class="p">;</span>
  <span class="ss">extraCommands</span> <span class="o">=</span> <span class="s s-Multiline">''</span>
<span class="s s-Multiline">    mkdir -p db</span>
<span class="s s-Multiline">  ''</span><span class="p">;</span>
  <span class="ss">config</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">Cmd</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"</span><span class="si">${</span>issoEnv<span class="si">}</span><span class="s2">/bin/gunicorn"</span>
            <span class="s2">"--name"</span> <span class="s2">"isso"</span>
            <span class="s2">"--bind"</span> <span class="s2">"0.0.0.0:</span><span class="si">${</span>port<span class="si">}</span><span class="s2">"</span>
            <span class="s2">"--worker-class"</span> <span class="s2">"gevent"</span>
            <span class="s2">"--workers"</span> <span class="s2">"2"</span>
            <span class="s2">"--worker-tmp-dir"</span> <span class="s2">"/dev/shm"</span>
            <span class="s2">"--preload"</span>
            <span class="s2">"isso.run"</span>
          <span class="p">];</span>
    <span class="ss">Env</span> <span class="o">=</span> <span class="p">[</span>
      <span class="s2">"ISSO_SETTINGS=</span><span class="si">${</span>issoConfig<span class="si">}</span><span class="s2">"</span>
      <span class="s2">"SSL_CERT_FILE=</span><span class="si">${</span>pkgs<span class="o">.</span>cacert<span class="si">}</span><span class="s2">/etc/ssl/certs/ca-bundle.crt"</span>
    <span class="p">];</span>
  <span class="p">};</span>
<span class="p">};</span>
</code></pre></div>

<p>Comme nous faisons référence à la dérivation <code>issoEnv</code> dans
<code>config.Cmd</code>, toute la dérivation, y compris Isso et Gunicorn, est
copiée à l'intérieur de l'image Docker. Il en va de même pour
<code>issoConfig</code>, le fichier de configuration que nous avons créé
précédemment, et <code>pkgs.cacert</code>, la dérivation contenant les
certificats racines. L'image résultante pèse 171 Mo une fois
installée, ce qui est comparable à l'image Debian Buster générée par
le <a href="https://github.com/isso-comments/isso/blob/master/Dockerfile"><code>Dockerfile</code></a> officiel.</p>
<p>NixOS dispose d'une abstraction pour déployer les conteneurs Docker.
Elle n'est pas actuellement documentée dans le manuel de NixOS, mais
vous pouvez consulter le <a href="https://github.com/NixOS/nixpkgs/blob/release-20.09/nixos/modules/virtualisation/oci-containers.nix">code source du module</a> pour connaître les options disponibles. J'ai choisi d'utiliser
<a href="https://podman.io/">Podman</a> au lieu de Docker comme moteur car cela évite d'avoir à
lancer un démon supplémentaire.</p>
<div class="language-nix codehilite"><pre><span/><code>virtualisation<span class="o">.</span><span class="ss">oci-containers</span> <span class="o">=</span> <span class="p">{</span>
  <span class="ss">backend</span> <span class="o">=</span> <span class="s2">"podman"</span><span class="p">;</span>
  <span class="ss">containers</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">isso</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">image</span> <span class="o">=</span> <span class="s2">"isso"</span><span class="p">;</span>
      <span class="ss">imageFile</span> <span class="o">=</span> issoDockerImage<span class="p">;</span>
      <span class="ss">ports</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"127.0.0.1:</span><span class="si">${</span>port<span class="si">}</span><span class="s2">:</span><span class="si">${</span>port<span class="si">}</span><span class="s2">"</span><span class="p">];</span>
      <span class="ss">volumes</span> <span class="o">=</span> <span class="p">[</span>
        <span class="s2">"/var/db/isso:/db"</span>
      <span class="p">];</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">};</span>
</code></pre></div>

<p>Un fichier de configuration pour <em>systemd</em> est automatiquement créé
pour exécuter et superviser le conteneur :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>systemctl<span class="w"> </span>status<span class="w"> </span>podman-isso.service
<span class="go">● podman-isso.service</span>
<span class="go">     Loaded: loaded (/nix/store/a66gzqqwcdzbh99sz8zz5l5xl8r8ag7w-unit-&gt;</span>
<span class="go">     Active: active (running) since Sun 2020-11-01 16:04:16 UTC; 4min 44s ago</span>
<span class="go">    Process: 14564 ExecStartPre=/nix/store/95zfn4vg4867gzxz1gw7nxayqcl&gt;</span>
<span class="go">   Main PID: 14697 (podman)</span>
<span class="go">         IP: 0B in, 0B out</span>
<span class="go">      Tasks: 10 (limit: 2313)</span>
<span class="go">     Memory: 221.3M</span>
<span class="go">        CPU: 10.058s</span>
<span class="go">     CGroup: /system.slice/podman-isso.service</span>
<span class="go">             ├─14697 /nix/store/pn52xgn1wb2vr2kirq3xj8ij0rys35mf-podma&gt;</span>
<span class="go">             └─14802 /nix/store/7vsba54k6ag4cfsfp95rvjzqf6rab865-conmo&gt;</span>

<span class="go">nov. 01 16:04:17 web03 podman[14697]: container init (image=localhost/isso:latest)</span>
<span class="go">nov. 01 16:04:17 web03 podman[14697]: container start (image=localhost/isso:latest)</span>
<span class="go">nov. 01 16:04:17 web03 podman[14697]: container attach (image=localhost/isso:latest)</span>
<span class="go">nov. 01 16:04:19 web03 conmon[14802]: INFO: connected to SMTP server</span>
<span class="go">nov. 01 16:04:19 web03 conmon[14802]: INFO: connected to https://vincent.bernat.ch</span>
<span class="go">nov. 01 16:04:19 web03 conmon[14802]: [INFO] Starting gunicorn 20.0.4</span>
<span class="go">nov. 01 16:04:19 web03 conmon[14802]: [INFO] Listening at: http://0.0.0.0:8080 (1)</span>
<span class="go">nov. 01 16:04:19 web03 conmon[14802]: [INFO] Using worker: gevent</span>
<span class="go">nov. 01 16:04:19 web03 conmon[14802]: [INFO] Booting worker with pid: 8</span>
<span class="go">nov. 01 16:04:19 web03 conmon[14802]: [INFO] Booting worker with pid: 9</span>
</code></pre></div>

<p>Pour terminer, nous configurons Nginx pour qu'il transmette les
requêtes à destination de <code>comments.luffy.cx</code> au conteneur. NixOS
offre une intégration simple pour mettre en place un certificat géré
par <em>Let's Encrypt</em>.</p>
<div class="language-nix codehilite"><pre><span/><code>services<span class="o">.</span>nginx<span class="o">.</span>virtualHosts<span class="o">.</span><span class="s2">"comments.luffy.cx"</span> <span class="o">=</span> <span class="p">{</span>
  <span class="ss">root</span> <span class="o">=</span> <span class="s2">"/data/webserver/comments.luffy.cx"</span><span class="p">;</span>
  <span class="ss">enableACME</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
  <span class="ss">forceSSL</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
  <span class="ss">extraConfig</span> <span class="o">=</span> <span class="s s-Multiline">''</span>
<span class="s s-Multiline">    access_log /var/log/nginx/comments.luffy.cx.log anonymous;</span>
<span class="s s-Multiline">  ''</span><span class="p">;</span>
  locations<span class="o">.</span><span class="s2">"/"</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">proxyPass</span> <span class="o">=</span> <span class="s2">"http://127.0.0.1:</span><span class="si">${</span>port<span class="si">}</span><span class="s2">"</span><span class="p">;</span>
    <span class="ss">extraConfig</span> <span class="o">=</span> <span class="s s-Multiline">''</span>
<span class="s s-Multiline">      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;</span>
<span class="s s-Multiline">      proxy_set_header Host $host;</span>
<span class="s s-Multiline">      proxy_set_header X-Forwarded-Proto $scheme;</span>
<span class="s s-Multiline">      proxy_hide_header Set-Cookie;</span>
<span class="s s-Multiline">      proxy_hide_header X-Set-Cookie;</span>
<span class="s s-Multiline">      proxy_ignore_headers Set-Cookie;</span>
<span class="s s-Multiline">    ''</span><span class="p">;</span>
  <span class="p">};</span>
<span class="p">};</span>
security<span class="o">.</span>acme<span class="o">.</span>certs<span class="o">.</span><span class="s2">"comments.luffy.cx"</span> <span class="o">=</span> <span class="p">{</span>
  <span class="ss">email</span> <span class="o">=</span> lib<span class="o">.</span>concatStringsSep <span class="s2">"@"</span> <span class="p">[</span> <span class="s2">"letsencrypt"</span> <span class="s2">"vincent.bernat.ch"</span> <span class="p">];</span>
<span class="p">};</span>
</code></pre></div>

<hr/>
<p>Bien que je rencontre encore des difficultés avec Nix et NixOS, je
suis convaincu que c'est ainsi qu'une <strong>infrastructure déclarative</strong>
doit être conçue. J'aime le fait d'avoir en <a href="https://github.com/vincentbernat/nixops-take1/blob/master/tags/isso.nix">un seul fichier</a> contenant la dérivation pour construire Isso, la
configuration, l'image Docker, la définition du conteneur et la
configuration Nginx. Le langage Nix permet à la fois de <strong>construire des
paquets</strong> et de <strong>gérer des configurations</strong>.</p>
<p>De plus, l'image Docker est mise à jour automatiquement comme un hôte
NixOS normal. Cela résout un problème qui affecte l'écosystème
Docker : plus d'images périmées ! Ma prochaine étape est de combiner
cette approche avec <a href="https://developer.hashicorp.com/nomad" title="Nomad: Workload Orchestration Made Easy">Nomad</a>, un orchestrateur pour déployer et gérer
les conteneurs.</p>
<div class="admonition">
<p class="admonition-title">Mise à jour (01.2023)</p>
<p>Je n'utilise désormais plus une image Docker mais un <a href="https://github.com/vincentbernat/nixops-take1/commit/04f01c8c991845e05054e1a58145eb262828eec4">conteneur
<em>systemd</em></a>. Cela permet de partager <code>/nix/store</code> et de faire tourner
<em>Isso</em> avec un utilisateur dynamique.</p>
</div>
<div class="footnote">
<hr/>
<ol>
<li id="fn:alternative">
<p>Il y a une différence subtile : j'utilise
<code>buildPythonPackage</code> au lieu de <code>buildPythonApplication</code>. C'est
important pour l'étape suivante. Je n'ai pas trouvé de moyen de
convertir une application en paquet. <a class="footnote-backref" href="#fnref:alternative" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
</feed>