💾 Archived View for gmi.runtimeterror.dev › feed.xml captured on 2024-08-25 at 02:09:05.
⬅️ Previous capture (2024-08-18)
-=-=-=-=-=-=-
<?xml version="1.0" encoding="utf-8" standalone="yes"?> <?xml-stylesheet type="text/xsl" href="/xml/feed.xsl" media="all"?> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"> <channel> <title>runtimeterror</title> <link>https://runtimeterror.dev/</link> <description>Recent content on runtimeterror</description> <generator>Hugo -- gohugo.io</generator> <language>en</language> <lastBuildDate>Thu, 22 Aug 2024 02:56:12 +0000</lastBuildDate><atom:link href="https://runtimeterror.dev/feed.xml" rel="self" type="application/rss+xml" /><image> <url>https://runtimeterror.dev/images/broken-computer.png</url> <title>runtimeterror</title> <link>https://runtimeterror.dev/</link> </image> <item> <title>SilverBullet: Self-Hosted Knowledge Management Web App</title> <link>https://runtimeterror.dev/silverbullet-self-hosted-knowledge-management/</link> <pubDate>Thu, 22 Aug 2024 02:56:12 +0000</pubDate> <dc:creator>John Bowdre</dc:creator> <category>Cloudflare</category> <category>Containers</category> <category>Docker</category> <category>Linux</category> <category>Selfhosting</category> <category>Tailscale</category> <guid>https://runtimeterror.dev/silverbullet-self-hosted-knowledge-management/</guid><description><p>I <a href="https://srsbsns.lol/is-silverbullet-the-note-keeping-silver-bullet/" rel="external">recently posted on my other blog↗</a> about trying out <a href="https://silverbullet.md" rel="external">SilverBullet↗</a>, an open-source self-hosted web-based note-keeping app. SilverBullet has continued to impress me as I use it and learn more about its <a href="https://silverbullet.md/SilverBullet@1992" rel="external">features↗</a>. It really fits my multi-device use case much better than Obsidian ever did (even with its paid sync plugin).</p> <p>In that post, I shared a brief overview of how I set up SilverBullet:</p> <blockquote> <p>I deployed my instance in Docker alongside both a <a href="https://runtimeterror.dev/tailscale-serve-docker-compose-sidecar/">Tailscale sidecar</a> and <a href="https://runtimeterror.dev/publish-services-cloudflare-tunnel/">Cloudflare Tunnel sidecar</a>. This setup lets me easily access/edit/manage my notes from any device I own by just pointing a browser at <code>https://silverbullet.tailnet-name.ts.net/</code>. And I can also hit it from any <em>other</em> device by using the public Cloudflare endpoint which is further protected by an email-based TOTP challenge. Either way, I don't have to worry about installing a bloated app or managing a complicated sync setup. Just log in and write.</p> </blockquote> <p>This post will go into a bit more detail about that configuration.</p> <h3 id="preparation"> Preparation <a class="hlink" href="#preparation"><i class="fa-solid fa-link"></i></a> </h3><p>I chose to deploy SilverBullet on an Ubuntu 22.04 VM in my <a href="https://runtimeterror.dev/homelab/">homelab</a> which was already set up for serving Docker workloads so I'm not going to cover the Docker <a href="https://docs.docker.com/engine/install/ubuntu/" rel="external">installation process↗</a> here. I tend to run my Docker workloads out of <code>/opt/</code> so I start this journey by creating a place to hold the SilverBullet setup:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>sudo mkdir -p /opt/silverbullet <span style="color:#75715e"># [tl! .cmd]</span> </span></span></code></pre></div><p>I set appropriate ownership of the folder and then move into it:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>sudo chown john:docker /opt/silverbullet <span style="color:#75715e"># [tl! .cmd:1]</span> </span></span><span style="display:flex;"><span>cd /opt/silverbullet </span></span></code></pre></div><h3 id="silverbullet-setup"> SilverBullet Setup <a class="hlink" href="#silverbullet-setup"><i class="fa-solid fa-link"></i></a> </h3><p>The documentation offers easy-to-follow guidance on <a href="https://silverbullet.md/Install/Docker" rel="external">installing SilverBullet with Docker Compose↗</a>, and that makes for a pretty good starting point. The only change I make here is setting the <code>SB_USER</code> variable from an environment variable instead of directly in the YAML:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># docker-compose.yml</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">services</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">silverbullet</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">zefhemel/silverbullet</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">silverbullet</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">environment</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">SB_USER</span>: <span style="color:#e6db74">&#34;${SB_CREDS}&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">./space:/space</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">ports</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">3000</span>:<span style="color:#ae81ff">3000</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">watchtower</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">containrrr/watchtower</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">silverbullet-watchtower</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">/var/run/docker.sock:/var/run/docker.sock</span> </span></span></code></pre></div><p>I used a password manager to generate a random password <em>and username</em>, and I stored those in a <code>.env</code> file alongside the Docker Compose configuration; I'll need those credentials to log in to each SilverBullet session. For example:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># .env</span> </span></span><span style="display:flex;"><span>SB_CREDS<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;alldiaryriver:XCTpmddGc3Ga4DkUr7DnPBYzt1b&#39;</span> </span></span></code></pre></div><p>That's all that's needed for running SilverBullet locally, and I <em>could</em> go ahead and <code>docker compose up -d</code> to get it running. But I really want to be able to access my notes from other systems too, so let's move on to enabling remote access right away.</p> <h3 id="remote-access"> Remote Access <a class="hlink" href="#remote-access"><i class="fa-solid fa-link"></i></a> </h3><h4 id="tailscale"> Tailscale <a class="hlink" href="#tailscale"><i class="fa-solid fa-link"></i></a> </h4><p>It's no secret that I'm a <a href="https://runtimeterror.dev/secure-networking-made-simple-with-tailscale/">big fan of Tailscale</a> so I use Tailscale Serve to enable secure remote access through my tailnet. I just need to add in a <a href="https://runtimeterror.dev/tailscale-serve-docker-compose-sidecar/#compose-configuration">Tailscale sidecar</a> and update the <code>silverbullet</code> service to share Tailscale's network:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># docker-compose.yml</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">services</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">tailscale</span>: <span style="color:#75715e"># [tl! ++:12 **:12]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">tailscale/tailscale:latest</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">silverbullet-tailscale</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">environment</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_AUTHKEY</span>: <span style="color:#ae81ff">${TS_AUTHKEY:?err}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_HOSTNAME</span>: <span style="color:#ae81ff">${TS_HOSTNAME:-ts-docker}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_EXTRA_ARGS</span>: <span style="color:#ae81ff">${TS_EXTRA_ARGS:-}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_STATE_DIR</span>: <span style="color:#ae81ff">/var/lib/tailscale/</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_SERVE_CONFIG</span>: <span style="color:#ae81ff">/config/serve-config.json</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">./ts_data:/var/lib/tailscale/</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">./serve-config.json:/config/serve-config.json</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">silverbullet</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">zefhemel/silverbullet</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">silverbullet</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">environment</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">SB_USER</span>: <span style="color:#e6db74">&#34;${SB_CREDS}&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">./space:/space</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">ports</span>: <span style="color:#75715e"># [tl! --:1 **:1]</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">3000</span>:<span style="color:#ae81ff">3000</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">network_mode</span>: <span style="color:#ae81ff">service:tailscale</span> <span style="color:#75715e"># [tl! ++ **]</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">watchtower</span>: <span style="color:#75715e"># [tl! collapse:4]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">containrrr/watchtower</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">silverbullet-watchtower</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">/var/run/docker.sock:/var/run/docker.sock</span> </span></span></code></pre></div><p>That of course means adding a few more items to the <code>.env</code> file:</p> <ul> <li>a <a href="https://tailscale.com/kb/1085/auth-keys" rel="external">pre-authentication key↗</a>,</li> <li>the hostname to use for the application's presence on my tailnet,</li> <li>and the <code>--ssh</code> extra argument to enable SSH access to the container (not strictly necessary, but can be handy for troubleshooting).</li> </ul> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># .env</span> </span></span><span style="display:flex;"><span>SB_CREDS<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;alldiaryriver:XCTpmddGc3Ga4DkUr7DnPBYzt1b&#39;</span> </span></span><span style="display:flex;"><span>TS_AUTHKEY<span style="color:#f92672">=</span>tskey-auth-<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span> <span style="color:#75715e"># [tl! ++:2 **:2]</span> </span></span><span style="display:flex;"><span>TS_HOSTNAME<span style="color:#f92672">=</span>silverbullet </span></span><span style="display:flex;"><span>TS_EXTRA_ARGS<span style="color:#f92672">=</span>--ssh </span></span></code></pre></div><p>And I need to create a <code>serve-config.json</code> file to configure <a href="https://runtimeterror.dev/tailscale-ssh-serve-funnel/#tailscale-serve">Tailscale Serve</a> to proxy port <code>443</code> on the tailnet to port <code>3000</code> on the container:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-json" data-lang="json"><span style="display:flex;"><span><span style="color:#75715e">// torchlight! {&#34;lineNumbers&#34;:true} </span></span></span><span style="display:flex;"><span><span style="color:#75715e">// serve-config.json </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>{ </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;TCP&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;443&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;HTTPS&#34;</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;Web&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;silverbullet.tailnet-name.ts.net:443&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;Handlers&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;/&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;Proxy&#34;</span>: <span style="color:#e6db74">&#34;http://127.0.0.1:3000&#34;</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><h4 id="cloudflare-tunnel"> Cloudflare Tunnel <a class="hlink" href="#cloudflare-tunnel"><i class="fa-solid fa-link"></i></a> </h4><p>But what if I want to consult my notes from <em>outside</em> of my tailnet? Sure, I <em>could</em> use <a href="https://runtimeterror.dev/tailscale-ssh-serve-funnel/#tailscale-funnel">Tailscale Funnel</a> to publish the SilverBullet service on the internet, but (1) funnel would require me to use a URL like <code>https://silverbullet.tailnet-name.ts.net</code> instead of simply <code>https://silverbullet.example.com</code> and (2) I've seen enough traffic logs to not want to expose a login page directly to the public internet if I can avoid it.</p> <p><a href="https://runtimeterror.dev/publish-services-cloudflare-tunnel/">Cloudflare Tunnel</a> is able to address those concerns without a lot of extra work. I can set up a tunnel at <code>silverbullet.example.com</code> and use <a href="https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/" rel="external">Cloudflare Access↗</a> to put an additional challenge in front of the login page.</p> <p>I just have to add a <code>cloudflared</code> container to my stack:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># docker-compose.yml</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">services</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">tailscale</span>: <span style="color:#75715e"># [tl! collapse:12]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">tailscale/tailscale:latest</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">silverbullet-tailscale</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">environment</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_AUTHKEY</span>: <span style="color:#ae81ff">${TS_AUTHKEY:?err}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_HOSTNAME</span>: <span style="color:#ae81ff">${TS_HOSTNAME:-ts-docker}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_EXTRA_ARGS</span>: <span style="color:#ae81ff">${TS_EXTRA_ARGS:-}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_STATE_DIR</span>: <span style="color:#ae81ff">/var/lib/tailscale/</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_SERVE_CONFIG</span>: <span style="color:#ae81ff">/config/serve-config.json</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">./ts_data:/var/lib/tailscale/</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">./serve-config.json:/config/serve-config.json</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">cloudflared</span>: <span style="color:#75715e"># [tl! ++:9 **:9]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">cloudflare/cloudflared</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">silverbullet-cloudflared</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">command</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">tunnel</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">run</span> </span></span><span style="display:flex;"><span> - --<span style="color:#ae81ff">token</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">${CLOUDFLARED_TOKEN}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">network_mode</span>: <span style="color:#ae81ff">service:tailscale</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">silverbullet</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">zefhemel/silverbullet</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">silverbullet</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">environment</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">SB_USER</span>: <span style="color:#e6db74">&#34;${SB_CREDS}&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">./space:/space</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">network_mode</span>: <span style="color:#ae81ff">service:tailscale</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">watchtower</span>: <span style="color:#75715e"># [tl! collapse:4]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">containrrr/watchtower</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">silverbullet-watchtower</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">/var/run/docker.sock:/var/run/docker.sock</span> </span></span></code></pre></div><p>To get the required <code>$CLOUDFLARED_TOKEN</code>, I <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-remote-tunnel/" rel="external">create a new <code>cloudflared</code> tunnel↗</a> in the Cloudflare dashboard and add the generated token value to my <code>.env</code> file:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># .env</span> </span></span><span style="display:flex;"><span>SB_CREDS<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;alldiaryriver:XCTpmddGc3Ga4DkUr7DnPBYzt1b&#39;</span> </span></span><span style="display:flex;"><span>TS_AUTHKEY<span style="color:#f92672">=</span>tskey-auth-<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span> </span></span><span style="display:flex;"><span>TS_HOSTNAME<span style="color:#f92672">=</span>silverbullet </span></span><span style="display:flex;"><span>TS_EXTRA_ARGS<span style="color:#f92672">=</span>--ssh </span></span><span style="display:flex;"><span>CLOUDFLARED_TOKEN<span style="color:#f92672">=</span>eyJhIjo<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>BNSJ9 <span style="color:#75715e"># [tl! ++ **]</span> </span></span></code></pre></div><p>Back in the Cloudflare Tunnel setup flow, I select my desired public hostname (<code>silverbullet.example.com</code>) and then specify that the backend service is <code>http://localhost:3000</code>.</p> <p>Now I'm finally ready to start up my containers:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>docker compose up -d <span style="color:#75715e"># [tl! .cmd .nocopy:1,5]</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>+<span style="color:#f92672">]</span> Running 5/5 </span></span><span style="display:flex;"><span> ✔ Network silverbullet_default Created </span></span><span style="display:flex;"><span> ✔ Container silverbullet-watchtower Started </span></span><span style="display:flex;"><span> ✔ Container silverbullet-tailscale Started </span></span><span style="display:flex;"><span> ✔ Container silverbullet Started </span></span><span style="display:flex;"><span> ✔ Container silverbullet-cloudflared Started </span></span></code></pre></div><h4 id="cloudflare-access"> Cloudflare Access <a class="hlink" href="#cloudflare-access"><i class="fa-solid fa-link"></i></a> </h4><p>The finishing touch will be configuring a bit of extra protection in front of the public-facing login page, and Cloudflare Access makes that very easy. I'll just use the wizard to <a href="https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/" rel="external">add a new web application↗</a> through the Cloudflare Zero Trust dashboard.</p> <p>The first part of that workflow asks &quot;What type of application do you want to add?&quot;. I select <strong>Self-hosted</strong>.</p> <p>The next part asks for a name (<strong>SilverBullet</strong>), Session Duration (<strong>24 hours</strong>), and domain (<code>silverbullet.example.com</code>). I leave the defaults for the rest of the Configuration Application step and move on to the next one.</p> <p>I'm then asked to Add Policies, and I have to start by giving a name for my policy. I opt to name it <strong>Email OTP</strong> because I'm going to set up email-based one-time passcodes. In the Configure Rules section, I choose <strong>Emails</strong> as the selector and enter my own email address as the single valid value.</p> <p>And then I just click through the rest of the defaults.</p> <h3 id="recap"> Recap <a class="hlink" href="#recap"><i class="fa-solid fa-link"></i></a> </h3><p>So now I have SilverBullet running in Docker Compose on a server in my homelab. I can access it from any device on my tailnet at <code>https://silverbullet.tailnet-name.ts.net</code> (thanks to the magic of Tailscale Serve). I can also get to it from outside my tailnet at <code>https://silverbullet.example.com</code> (thanks to Cloudflare Tunnel), and but I'll use a one-time passcode sent to my approved email address before also authenticating through the SilverBullet login page (thanks to Cloudflare Access).</p> <p>I think it's a pretty sweet setup that gives me full control and ownership of my notes and lets me read/write my notes from multiple devices without having to worry about synchronization.</p> </description> </item> <item> <title>Generate a Dynamic robots.txt File in Hugo with External Data Sources</title> <link>https://runtimeterror.dev/dynamic-robots-txt-hugo-external-data-sources/</link> <pubDate>Tue, 06 Aug 2024 16:59:39 +0000</pubDate> <dc:creator>John Bowdre</dc:creator> <category>Api</category> <category>Hugo</category> <category>Meta</category> <guid>https://runtimeterror.dev/dynamic-robots-txt-hugo-external-data-sources/</guid><description><p>I shared <a href="https://runtimeterror.dev/blocking-ai-crawlers/">back in April</a> my approach for generating a <code>robots.txt</code> file to <s>block</s> <em>discourage</em> AI crawlers from stealing my content. That setup used a static list of known-naughty user agents (derived from the <a href="https://github.com/ai-robots-txt/ai.robots.txt" rel="external">community-maintained <code>ai.robots.txt</code> project↗</a>) in my Hugo config file. It's fine (I guess) but it can be hard to keep up with the bad actors - and I'm too lazy to manually update my local copy of the list when things change.</p> <p>Wouldn't it be great if I could cut out the middle man (me) and have Hugo work straight off of that remote resource? Inspired by <a href="https://www.lkhrs.com/blog/2024/darkvisitors-hugo/" rel="external">Luke Harris's work↗</a> with using Hugo's <a href="https://gohugo.io/functions/resources/getremote/" rel="external"><code>resources.GetRemote</code> function↗</a> to build a <code>robots.txt</code> from the <a href="https://darkvisitors.com/" rel="external">Dark Visitors↗</a> API, I set out to figure out how to do that for ai.robots.txt.</p> <p>While I was tinkering with that, <a href="https://adam.omg.lol/" rel="external">Adam↗</a> and <a href="https://coryd.dev/" rel="external">Cory↗</a> were tinkering with a GitHub Actions workflow to streamline the addition of new agents. That repo now uses <a href="https://github.com/ai-robots-txt/ai.robots.txt/blob/main/robots.json" rel="external">a JSON file↗</a> as the Source of Truth for its agent list, and a JSON file available over HTTP looks an <em>awful</em> lot like a poor man's API to me.</p> <p>So here's my updated solution.</p> <p>As before, I'm taking advantage of Hugo's <a href="https://gohugo.io/templates/robots/" rel="external">robot.txt templating↗</a> to build the file. That requires the following option in my <code>config/hugo.toml</code> file:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">enableRobotsTXT</span> = <span style="color:#66d9ef">true</span> </span></span></code></pre></div><p>That tells Hugo to process <code>layouts/robots.txt</code>, which I have set up with this content to insert the sitemap and greet robots who aren't assholes:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-text" data-lang="text"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;:true} </span></span><span style="display:flex;"><span>Sitemap: {{ .Site.BaseURL }}sitemap.xml </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span># hello robots [^_^] </span></span><span style="display:flex;"><span># let&#39;s be friends &lt;3 </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>User-agent: * </span></span><span style="display:flex;"><span>Disallow: </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span># except for these bots which are not friends: </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>{{ partial &#34;bad-robots.html&#34; . }} </span></span></code></pre></div><p>I opted to break the heavy lifting out into <code>layouts/partials/bad-robots.html</code> to keep things a bit tidier in the main template. This starts out simply enough with using <code>resources.GetRemote</code> to fetch the desired JSON file, and printing an error if that doesn't work:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-jinja" data-lang="jinja"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;:true} </span></span><span style="display:flex;"><span><span style="color:#75715e">{{</span><span style="color:#f92672">-</span> <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>url <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;https://raw.githubusercontent.com/ai-robots-txt/ai.robots.txt/main/robots.json&#34;</span> -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">{{</span><span style="color:#f92672">-</span> <span style="color:#66d9ef">with</span> resources.GetRemote <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>url -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> <span style="color:#66d9ef">with</span> .Err -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> errorf <span style="color:#e6db74">&#34;%s&#34;</span> <span style="color:#960050;background-color:#1e0010">.</span> -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> <span style="color:#66d9ef">else</span> -<span style="color:#75715e">}}</span> </span></span></code></pre></div><p>The JSON file looks a bit like this, with the user agent strings as the top-level keys:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;Amazonbot&#34;</span>: { <span style="color:#75715e">// [tl! **] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">&#34;operator&#34;</span>: <span style="color:#e6db74">&#34;Amazon&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;respect&#34;</span>: <span style="color:#e6db74">&#34;Yes&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;function&#34;</span>: <span style="color:#e6db74">&#34;Service improvement and enabling answers for Alexa users.&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;frequency&#34;</span>: <span style="color:#e6db74">&#34;No information. provided.&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;Includes references to crawled website when surfacing answers via Alexa; does not clearly outline other uses.&#34;</span> </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;anthropic-ai&#34;</span>: { <span style="color:#75715e">// [tl! **] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">&#34;operator&#34;</span>: <span style="color:#e6db74">&#34;[Anthropic](https:\/\/www.anthropic.com)&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;respect&#34;</span>: <span style="color:#e6db74">&#34;Unclear at this time.&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;function&#34;</span>: <span style="color:#e6db74">&#34;Scrapes data to train Anthropic&#39;s AI products.&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;frequency&#34;</span>: <span style="color:#e6db74">&#34;No information. provided.&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;Scrapes data to train LLMs and AI products offered by Anthropic.&#34;</span> </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;Applebot-Extended&#34;</span>: { <span style="color:#75715e">// [tl! **] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">&#34;operator&#34;</span>: <span style="color:#e6db74">&#34;[Apple](https:\/\/support.apple.com\/en-us\/119829#datausage)&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;respect&#34;</span>: <span style="color:#e6db74">&#34;Yes&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;function&#34;</span>: <span style="color:#e6db74">&#34;Powers features in Siri, Spotlight, Safari, Apple Intelligence, and others.&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;frequency&#34;</span>: <span style="color:#e6db74">&#34;Unclear at this time.&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;Apple has a secondary user agent, Applebot-Extended ... [that is] used to train Apple&#39;s foundation models powering generative AI features across Apple products, including Apple Intelligence, Services, and Developer Tools.&#34;</span> </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> <span style="color:#960050;background-color:#1e0010">{...</span>} </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">}</span> </span></span></code></pre></div><p>There's quite a bit more detail in this JSON than I really care about; all I need for this are the bot names. So I unmarshal the JSON data, iterate through the top-level keys to extract the names, and print a line starting with <code>User-agent: </code> followed by the name for each bot.</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-jinja" data-lang="jinja"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;:true, &#34;lineNumbersStart&#34;:6} </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>robots <span style="color:#f92672">:=</span> unmarshal .Content -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> range <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>botname<span style="color:#f92672">,</span> <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span><span style="color:#66d9ef">_</span> <span style="color:#f92672">:=</span> <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>robots <span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> printf <span style="color:#e6db74">&#34;User-agent: %s\n&#34;</span> <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>botname <span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> end <span style="color:#75715e">}}</span> </span></span></code></pre></div><p>And once the loop is finished, I print the important <code>Disallow: /</code> rule (and a plug for the repo) and clean up:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-jinja" data-lang="jinja"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;:true, &#34;lineNumbersStart&#34;:10} </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> printf <span style="color:#e6db74">&#34;Disallow: /\n&#34;</span> <span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> printf <span style="color:#e6db74">&#34;\n# (bad bots bundled by https://github.com/ai-robots-txt/ai.robots.txt)&#34;</span> <span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> end -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">{{</span><span style="color:#f92672">-</span> <span style="color:#66d9ef">else</span> -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> errorf <span style="color:#e6db74">&#34;Unable to get remote resource %q&#34;</span> <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>url -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">{{</span><span style="color:#f92672">-</span> end -<span style="color:#75715e">}}</span> </span></span></code></pre></div><p>So here's the completed <code>layouts/partials/bad-robots.html</code>:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-jinja" data-lang="jinja"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;:true} </span></span><span style="display:flex;"><span><span style="color:#75715e">{{</span><span style="color:#f92672">-</span> <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>url <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;https://raw.githubusercontent.com/ai-robots-txt/ai.robots.txt/main/robots.json&#34;</span> -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">{{</span><span style="color:#f92672">-</span> <span style="color:#66d9ef">with</span> resources.GetRemote <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>url -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> <span style="color:#66d9ef">with</span> .Err -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> errorf <span style="color:#e6db74">&#34;%s&#34;</span> <span style="color:#960050;background-color:#1e0010">.</span> -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> <span style="color:#66d9ef">else</span> -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>robots <span style="color:#f92672">:=</span> unmarshal .Content -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> range <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>botname<span style="color:#f92672">,</span> <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span><span style="color:#66d9ef">_</span> <span style="color:#f92672">:=</span> <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>robots <span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> printf <span style="color:#e6db74">&#34;User-agent: %s\n&#34;</span> <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>botname <span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> end <span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> printf <span style="color:#e6db74">&#34;Disallow: /\n&#34;</span> <span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> printf <span style="color:#e6db74">&#34;\n# (bad bots bundled by https://github.com/ai-robots-txt/ai.robots.txt)&#34;</span> <span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> end -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">{{</span><span style="color:#f92672">-</span> <span style="color:#66d9ef">else</span> -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">{{</span><span style="color:#f92672">-</span> errorf <span style="color:#e6db74">&#34;Unable to get remote resource %q&#34;</span> <span style="color:#960050;background-color:#1e0010">{body}amp;lt;/span>url -<span style="color:#75715e">}}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">{{</span><span style="color:#f92672">-</span> end -<span style="color:#75715e">}}</span> </span></span></code></pre></div><p>After that's in place, I can fire off a quick <code>hugo server</code> in the shell and check out my work at <code>http://localhost:1313/robots.txt</code>:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Sitemap: http://localhost:1313/sitemap.xml </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span># hello robots [^_^] </span></span><span style="display:flex;"><span># let&#39;s be friends &lt;3 </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>User-agent: * </span></span><span style="display:flex;"><span>Disallow: </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span># except for these bots which are not friends: </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>User-agent: Amazonbot </span></span><span style="display:flex;"><span>User-agent: Applebot-Extended </span></span><span style="display:flex;"><span>User-agent: Bytespider </span></span><span style="display:flex;"><span>User-agent: CCBot </span></span><span style="display:flex;"><span>User-agent: ChatGPT-User </span></span><span style="display:flex;"><span>User-agent: Claude-Web </span></span><span style="display:flex;"><span>User-agent: ClaudeBot </span></span><span style="display:flex;"><span>User-agent: Diffbot </span></span><span style="display:flex;"><span>User-agent: FacebookBot </span></span><span style="display:flex;"><span>User-agent: FriendlyCrawler </span></span><span style="display:flex;"><span>User-agent: GPTBot </span></span><span style="display:flex;"><span>User-agent: Google-Extended </span></span><span style="display:flex;"><span>User-agent: GoogleOther </span></span><span style="display:flex;"><span>User-agent: GoogleOther-Image </span></span><span style="display:flex;"><span>User-agent: GoogleOther-Video </span></span><span style="display:flex;"><span>User-agent: ICC-Crawler </span></span><span style="display:flex;"><span>User-agent: ImageSift </span></span><span style="display:flex;"><span>User-agent: Meta-ExternalAgent </span></span><span style="display:flex;"><span>User-agent: OAI-SearchBot </span></span><span style="display:flex;"><span>User-agent: PerplexityBot </span></span><span style="display:flex;"><span>User-agent: PetalBot </span></span><span style="display:flex;"><span>User-agent: Scrapy </span></span><span style="display:flex;"><span>User-agent: Timpibot </span></span><span style="display:flex;"><span>User-agent: VelenPublicWebCrawler </span></span><span style="display:flex;"><span>User-agent: YouBot </span></span><span style="display:flex;"><span>User-agent: anthropic-ai </span></span><span style="display:flex;"><span>User-agent: cohere-ai </span></span><span style="display:flex;"><span>User-agent: facebookexternalhit </span></span><span style="display:flex;"><span>User-agent: img2dataset </span></span><span style="display:flex;"><span>User-agent: omgili </span></span><span style="display:flex;"><span>User-agent: omgilibot </span></span><span style="display:flex;"><span>Disallow: / </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span># (bad bots bundled by https://github.com/ai-robots-txt/ai.robots.txt) </span></span></code></pre></div><p>Neat!</p> <h3 id="next-steps"> Next Steps <a class="hlink" href="#next-steps"><i class="fa-solid fa-link"></i></a> </h3><p>Of course, bad agents being disallowed in a <code>robots.txt</code> doesn't really accomplish anything if they're <a href="https://rknight.me/blog/perplexity-ai-robotstxt-and-other-questions/" rel="external">just going to ignore that and scrape my content anyway↗</a>. I closed my <a href="https://runtimeterror.dev/blocking-ai-crawlers/">last post</a> on the subject with a bit about the Cloudflare WAF rule I had created to actively block these known bad actors. Since then, two things have changed:</p> <p>First, Cloudflare rolled out an even easier way to <a href="https://blog.cloudflare.com/declaring-your-aindependence-block-ai-bots-scrapers-and-crawlers-with-a-single-click" rel="external">block bad bots with a single click↗</a>. If you're using Cloudflare, just enable that and call it day.</p> <p>Second, this site is <a href="https://runtimeterror.dev/further-down-the-bunny-hole/">now hosted (and fronted) by Bunny</a> so the Cloudflare solutions won't help me anymore.</p> <p>Instead, I've been using <a href="https://paste.melanie.lol/bunny-ai-blocking.js" rel="external">Melanie's handy script↗</a> to create a Bunny edge rule (similar to the Cloudflare WAF rule) to handle the blocking.</p> <p>Going forward, I think I'd like to explore using <a href="https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs" rel="external">Bunny's new Terraform provider↗</a> to manage the <a href="https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/pullzone_edgerule" rel="external">edge rule↗</a> in a more stateful way. But that's a topic for another post!</p> </description> </item> <item> <title>Taking Taildrive for a Testdrive</title> <link>https://runtimeterror.dev/taking-taildrive-testdrive/</link> <pubDate>Mon, 29 Jul 2024 23:48:29 +0000</pubDate> <dc:creator>John Bowdre</dc:creator> <category>Linux</category> <category>Tailscale</category> <guid>https://runtimeterror.dev/taking-taildrive-testdrive/</guid><description><p>My little <a href="https://runtimeterror.dev/homelab">homelab</a> is bit different from many others in that I don't have a SAN/NAS or other dedicated storage setup. This can sometimes make sharing files between systems a little bit tricky. I've used workarounds like <a href="https://runtimeterror.dev/tailscale-ssh-serve-funnel/#tailscale-serve">Tailscale Serve</a> for sharing files over HTTP or simply <code>scp</code>ing files around as needed, but none of those solutions are really very elegant.</p> <p>Last week, Tailscale announced <a href="https://tailscale.com/blog/controld" rel="external">a new integration↗</a> with <a href="https://controld.com/" rel="external">ControlD↗</a> to add advanced DNS filtering and security. While I was getting that set up on my tailnet, I stumbled across an option I hadn't previously noticed in the Tailscale CLI: the <code>tailscale drive</code> command:</p> <blockquote> <p>Share a directory with your tailnet</p> <p>USAGE <code>tailscale drive share &lt;name&gt; &lt;path&gt;</code> <code>tailscale drive rename &lt;oldname&gt; &lt;newname&gt;</code> <code>tailscale drive unshare &lt;name&gt;</code> <code>tailscale drive list</code></p> <p>Taildrive allows you to share directories with other machines on your tailnet.</p> </blockquote> <p>That sounded kind of neat - especially once I found the corresponding <a href="https://tailscale.com/kb/1369/taildrive" rel="external">Taildrive documentation↗</a> and started to get a better understanding of how this new(ish) feature works:</p> <blockquote> <p>Normally, maintaining a file server requires you to manage credentials and access rules separately from the connectivity layer. Taildrive offers a file server that unifies connectivity and access controls, allowing you to share directories directly from the Tailscale client. You can then use your tailnet policy file to define which members of your tailnet can access a particular shared directory, and even define specific read and write permissions.</p> <p>Beginning in version 1.64.0, the Tailscale client includes a WebDAV server that runs on <code>100.100.100.100:8080</code> while Tailscale is connected. Every directory that you share receives a globally-unique path consisting of the tailnet, the machine name, and the share name: <code>/tailnet/machine/share</code>.</p> <p>For example, if you shared a directory with the share name <code>docs</code> from the machine <code>mylaptop</code> on the tailnet <code>mydomain.com</code>, the share's path would be <code>/mydomain.com/mylaptop/docs</code>.</p> </blockquote> <p>Oh yeah. That will be a huge simplification for how I share files within my tailnet.</p> <p>I've now had a chance to get this implemented on my tailnet and thought I'd share some notes on how I did it.</p> <h3 id="acl-changes"> ACL Changes <a class="hlink" href="#acl-changes"><i class="fa-solid fa-link"></i></a> </h3><p>My Tailscale policy relies heavily on <a href="https://tailscale.com/kb/1068/acl-tags" rel="external">ACL tags↗</a> to manage access between systems, especially for &quot;headless&quot; server systems which don't typically have users logged in to them. I don't necessarily want every system to be able to export a file share so I decided to control that capability with a new <code>tag:share</code> flag. Before I could use that tag, though, I had to <a href="https://tailscale.com/kb/1068/acl-tags#define-a-tag" rel="external">add it to the ACL↗</a>:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;groups&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;group:admins&#34;</span>: [<span style="color:#e6db74">&#34;user@example.com&#34;</span>], </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;tagOwners&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;tag:share&#34;</span>: [<span style="color:#e6db74">&#34;group:admins&#34;</span>], </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> <span style="color:#960050;background-color:#1e0010">{...</span>}<span style="color:#960050;background-color:#1e0010">,</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">}</span> </span></span></code></pre></div><p>Next I needed to add the appropriate <a href="https://tailscale.com/kb/1337/acl-syntax#nodeattrs" rel="external">node attributes↗</a> to enable Taildrive sharing on devices with that tag and Taildrive access for all other systems:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;nodeAttrs&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#960050;background-color:#1e0010">{</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">// devices with the share tag can share files with Taildrive </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">&#34;target&#34;</span>: [<span style="color:#e6db74">&#34;tag:share&#34;</span>], </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;attr&#34;</span>: [<span style="color:#e6db74">&#34;drive:share&#34;</span>], </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> <span style="color:#960050;background-color:#1e0010">{</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">// all devices can access shares </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">&#34;target&#34;</span>: [<span style="color:#e6db74">&#34;*&#34;</span>], </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;attr&#34;</span>: [<span style="color:#e6db74">&#34;drive:access&#34;</span>], </span></span><span style="display:flex;"><span> }<span style="color:#960050;background-color:#1e0010">,</span> </span></span><span style="display:flex;"><span> <span style="color:#960050;background-color:#1e0010">},</span> </span></span><span style="display:flex;"><span> {<span style="color:#960050;background-color:#1e0010">...</span>}<span style="color:#960050;background-color:#1e0010">,</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">}</span> </span></span></code></pre></div><p>And I created a pair of <a href="https://tailscale.com/kb/1324/acl-grants" rel="external">Grants↗</a> to give logged-in users read-write access and tagged devices read-only access:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;grants&#34;</span>:[ </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> <span style="color:#75715e">// users get read-write access to shares </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">&#34;src&#34;</span>: [<span style="color:#e6db74">&#34;autogroup:member&#34;</span>], </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;dst&#34;</span>: [<span style="color:#e6db74">&#34;tag:share&#34;</span>], </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;app&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;tailscale.com/cap/drive&#34;</span>: [{ </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;shares&#34;</span>: [<span style="color:#e6db74">&#34;*&#34;</span>], </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;access&#34;</span>: <span style="color:#e6db74">&#34;rw&#34;</span> </span></span><span style="display:flex;"><span> }] </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> <span style="color:#75715e">// tagged devices get read-only access </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">&#34;src&#34;</span>: [<span style="color:#e6db74">&#34;autogroup:tagged&#34;</span>], </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;dst&#34;</span>: [<span style="color:#e6db74">&#34;tag:share&#34;</span>], </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;app&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;tailscale.com/cap/drive&#34;</span>: [{ </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;shares&#34;</span>: [<span style="color:#e6db74">&#34;*&#34;</span>], </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;access&#34;</span>: <span style="color:#e6db74">&#34;ro&#34;</span> </span></span><span style="display:flex;"><span> }] </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> ], </span></span><span style="display:flex;"><span> <span style="color:#960050;background-color:#1e0010">{...</span>}<span style="color:#960050;background-color:#1e0010">,</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">}</span> </span></span></code></pre></div><p>That will let me create/manage files from the devices I regularly work on, and easily retrieve them as needed on the others.</p> <p>Then I just used the Tailscale admin portal to add the new <code>tag:share</code> tag to my existing <code>files</code> node:</p> <p><img src="https://runtimeterror.dev/taking-taildrive-testdrive/files-tags.png" alt="The files node tagged with tag:internal, tag:salt-minion, and tag:share"></p> <h3 id="exporting-the-share"> Exporting the Share <a class="hlink" href="#exporting-the-share"><i class="fa-solid fa-link"></i></a> </h3><p>After making the required ACL changes, actually publishing the share was very straightforward. Per the <a href="https://paste.jbowdre.lol/tailscale-drive" rel="external"><code>tailscale drive --help</code> output↗</a>, the syntax is:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>tailscale drive share &lt;name&gt; &lt;path&gt; <span style="color:#75715e"># [tl! .cmd]</span> </span></span></code></pre></div><p>I (somewhat-confusingly) wanted to share a share named <code>share</code>, found at <code>/home/john/share</code> (I <em>might</em> be bad at naming things) so I used this to export it:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>tailscale drive share share /home/john/share <span style="color:#75715e"># [tl! .cmd]</span> </span></span></code></pre></div><p>And I could verify that <code>share</code> had, in fact, been shared with:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>tailscale drive list <span style="color:#75715e"># [tl! .cmd]</span> </span></span><span style="display:flex;"><span>name path as <span style="color:#75715e"># [tl! .nocopy:2]</span> </span></span><span style="display:flex;"><span>----- ---------------- ---- </span></span><span style="display:flex;"><span>share /home/john/share john </span></span></code></pre></div><h3 id="mounting-the-share"> Mounting the Share <a class="hlink" href="#mounting-the-share"><i class="fa-solid fa-link"></i></a> </h3><p>In order to mount the share from the Debian <a href="https://support.google.com/chromebook/answer/9145439" rel="external">Linux development environment on my Chromebook↗</a>, I first needed to install the <code>davfs2</code> package to add support for mounting WebDAV shares:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>sudo apt update <span style="color:#75715e"># [tl! .cmd:1]</span> </span></span><span style="display:flex;"><span>sudo apt install davfs2 </span></span></code></pre></div><p>I need to be able mount the share as my standard user account (<em>without</em> elevation) to ensure that the ownership and permissions are correctly inherited. The <code>davfs2</code> installer offered to enable the SUID bit to support this, but that change on its own doesn't seem to have been sufficient in my testing. In addition (or perhaps instead?), I had to add my account to the <code>davfs2</code> group:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>sudo usermod -aG davfs2 $USER <span style="color:#75715e"># [tl! .cmd]</span> </span></span></code></pre></div><p>And then use the <code>newgrp</code> command to load the new membership without having to log out and back in again:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>newgrp davfs2 <span style="color:#75715e"># [tl! .cmd]</span> </span></span></code></pre></div><p>Next I created a folder inside my home directory to use as a mountpoint:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>mkdir ~/taildrive <span style="color:#75715e"># [tl! .cmd]</span> </span></span></code></pre></div><p>I knew from the <a href="https://tailscale.com/kb/1369/taildrive" rel="external">Taildrive docs↗</a> that the WebDAV server would be running at <code>http://100.100.100.100:8080</code> and the share would be available at <code>/&lt;tailnet&gt;/&lt;machine&gt;/&lt;share&gt;</code>, so I added the following to my <code>/etc/fstab</code>:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>http://100.100.100.100:8080/example.com/files/share /home/john/taildrive/ davfs user,rw,noauto 0 0 </span></span></code></pre></div><p>Then I ran <code>sudo systemctl daemon-reload</code> to make sure the system knew about the changes to the fstab.</p> <p>Taildrive's WebDAV implementation doesn't require any additional authentication (that's handled automatically by Tailscale), but <code>davfs2</code> doesn't know that. So to keep it from prompting unnecessarily for credentials when attempting to mount the taildrive, I added this to the bottom of <code>~/.davfs2/secrets</code>, with empty strings taking the place of the username and password:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>/home/john/taildrive &#34;&#34; &#34;&#34; </span></span></code></pre></div><p>After that, I could mount the share like so:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>mount ~/taildrive <span style="color:#75715e"># [tl! .cmd]</span> </span></span></code></pre></div><p>And verify that I could see the files being shared from <code>share</code> on <code>files</code>:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>ls -l ~/taildrive <span style="color:#75715e"># [tl! .cmd]</span> </span></span><span style="display:flex;"><span>drwxr-xr-x - john <span style="color:#ae81ff">15</span> Feb 09:20 books <span style="color:#75715e"># [tl! .nocopy:5]</span> </span></span><span style="display:flex;"><span>drwxr-xr-x - john <span style="color:#ae81ff">22</span> Oct <span style="color:#ae81ff">2023</span> dist </span></span><span style="display:flex;"><span>drwx------ - john <span style="color:#ae81ff">28</span> Jul 15:10 lost+found </span></span><span style="display:flex;"><span>drwxr-xr-x - john <span style="color:#ae81ff">22</span> Nov <span style="color:#ae81ff">2023</span> media </span></span><span style="display:flex;"><span>drwxr-xr-x - john <span style="color:#ae81ff">16</span> Feb <span style="color:#ae81ff">2023</span> notes </span></span><span style="display:flex;"><span>.rw-r--r-- <span style="color:#ae81ff">18</span> john <span style="color:#ae81ff">10</span> Jan <span style="color:#ae81ff">2023</span> status </span></span></code></pre></div><p>Neat, right?</p> <p>I'd like to eventually get this set up so that <a href="https://help.ubuntu.com/community/Autofs" rel="external">AutoFS↗</a> can handle mounting the Taildrive WebDAV share on the fly. I know that <a href="https://www.chromium.org/chromium-os/developer-library/guides/containers/containers-and-vms/#can-i-mount-filesystems" rel="external">won't work within the containerized Linux environment on my Chromebook↗</a> but I think it <em>should</em> be possible on an actual Linux system. My initial efforts were unsuccessful though; I'll update this post if I figure it out.</p> <p>In the meantime, though, this will be a more convenient way for me to share files between my Tailscale-connected systems.</p> </description> </item> <item> <title>Automate Packer Builds with GithHub Actions</title> <link>https://runtimeterror.dev/automate-packer-builds-github-actions/</link> <pubDate>Thu, 25 Jul 2024 02:28:10 +0000</pubDate> <dc:creator>John Bowdre</dc:creator> <category>Api</category> <category>Automation</category> <category>Containers</category> <category>Docker</category> <category>Iac</category> <category>Linux</category> <category>Packer</category> <category>Proxmox</category> <category>Selfhosting</category> <category>Shell</category> <category>Tailscale</category> <guid>https://runtimeterror.dev/automate-packer-builds-github-actions/</guid><description><p>I recently shared how I <a href="https://runtimeterror.dev/building-proxmox-templates-packer/">set up Packer to build Proxmox templates</a> in my homelab. That post covered storing (and retrieving) environment-specific values in Vault, the <code>cloud-init</code> configuration for defining the installation parameters, the various post-install scripts for further customizing and hardening the template, and the Packer template files that tie it all together. By the end of the post, I was able to simply run <code>./build.sh ubuntu2204</code> to kick the build of a new Ubuntu 22.04 template without having to do any other interaction with the process.</p> <p>That's pretty cool, but <em>The Dream</em> is to not have to do anything at all. So that's what this post is about: setting up a self-hosted GitHub Actions Runner to perform the build and a GitHub Actions workflow to trigger it.</p> <h3 id="self-hosted-runner"> Self-Hosted Runner <a class="hlink" href="#self-hosted-runner"><i class="fa-solid fa-link"></i></a> </h3><p>When a GitHub Actions workflow fires, it schedules the job(s) to run on GitHub's own infrastructure. That's easy and convenient, but can make things tricky when you need a workflow to interact with on-prem infrastructure. I've worked around that in the past by <a href="https://runtimeterror.dev/gemini-capsule-gempost-github-actions/#publish-github-actions">configuring the runner to connect to my tailnet</a>, but given the amount of data that will need to be transferred during the Packer build I decided that a <a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners" rel="external">self-hosted runner↗</a> would be a better solution.</p> <p>I wanted my runner to execute the build inside of a Docker container for better control of the environment, and I wanted that container to run <a href="https://docs.docker.com/engine/security/rootless/" rel="external">without elevated permissions (rootless)↗</a>.</p> <div></div><div class="notice note"> <p class="first notice-title"><span class="icon-notice baseline"></span>Self-Hosted Runner Security</p><p>GitHub <a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#self-hosted-runner-security" rel="external">strongly recommends↗</a> that you only use self-hosted runners with <strong>private</strong> repositories. You don't want a misconfigured workflow to allow a pull request submitted from a fork to run potentially-malicious code on your system(s).</p> <p>So while I have a <a href="https://github.com/jbowdre/packer-proxmox-templates/" rel="external">public repo↗</a> to share my Packer work, my runner environment is attached to an otherwise-identical private repo. I'd recommend following a similar setup.</p></div> <h4 id="setup-rootless-docker-host"> Setup Rootless Docker Host <a class="hlink" href="#setup-rootless-docker-host"><i class="fa-solid fa-link"></i></a> </h4><p>I start by cloning a fresh Ubuntu 22.04 VM off of my new template. After doing the basic initial setup (setting the hostname and IP, connecting it Tailscale, and so on), I create a user account for the runner to use. That account will need sudo privileges during the initial setup, but those will be revoked later on. I also set a password for the account.</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>sudo useradd -m -G sudo -s <span style="color:#66d9ef">$(</span>which bash<span style="color:#66d9ef">)</span> github <span style="color:#75715e"># [tl! .cmd:1]</span> </span></span><span style="display:flex;"><span>sudo passwd github </span></span></code></pre></div><p>I then install the <code>systemd-container</code> package so that I can use <a href="https://www.man7.org/linux/man-pages/man1/machinectl.1.html" rel="external"><code>machinectl</code>↗</a> to log in as the new user (since <a href="https://docs.docker.com/engine/security/rootless/#unable-to-install-with-systemd-when-systemd-is-present-on-the-system" rel="external"><code>sudo su</code> won't work for the rootless setup↗</a>).</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>sudo apt update <span style="color:#75715e"># [tl! .cmd:2]</span> </span></span><span style="display:flex;"><span>sudo apt install systemd-container </span></span><span style="display:flex;"><span>sudo machinectl shell github@ </span></span></code></pre></div><p>And I install the <code>uidmap</code> package since rootless Docker requires <code>newuidmap</code> and <code>newgidmap</code>:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>sudo apt install uidmap <span style="color:#75715e"># [tl! .cmd]</span> </span></span></code></pre></div><p>At this point, I can follow the usual <a href="https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository" rel="external">Docker installation instructions↗</a>:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># Add Docker&#39;s official GPG key:</span> </span></span><span style="display:flex;"><span>sudo apt-get update <span style="color:#75715e"># [tl! .cmd:4]</span> </span></span><span style="display:flex;"><span>sudo apt-get install ca-certificates curl </span></span><span style="display:flex;"><span>sudo install -m <span style="color:#ae81ff">0755</span> -d /etc/apt/keyrings </span></span><span style="display:flex;"><span>sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc </span></span><span style="display:flex;"><span>sudo chmod a+r /etc/apt/keyrings/docker.asc </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># Add the repository to apt sources:</span> </span></span><span style="display:flex;"><span>echo <span style="color:#ae81ff">\ </span><span style="color:#75715e"># [tl! .cmd]</span> </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;deb [arch=</span><span style="color:#66d9ef">$(</span>dpkg --print-architecture<span style="color:#66d9ef">)</span><span style="color:#e6db74"> signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> </span><span style="color:#66d9ef">$(</span>. /etc/os-release <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#34;</span>$VERSION_CODENAME<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span><span style="color:#e6db74"> stable&#34;</span> | <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null </span></span><span style="display:flex;"><span>sudo apt-get update <span style="color:#75715e"># [tl! .cmd]</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># Install the Docker packages:</span> </span></span><span style="display:flex;"><span>sudo apt-get install <span style="color:#ae81ff">\ </span><span style="color:#75715e"># [tl! .cmd]</span> </span></span><span style="display:flex;"><span> docker-ce <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> docker-ce-cli <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> containerd.io <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> docker-buildx-plugin <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> docker-compose-plugin </span></span></code></pre></div><p>Now it's time for the rootless setup, which starts by disabling the existing Docker service and socket and then running the <code>dockerd-rootless-setuptool.sh</code> script:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>sudo systemctl disable --now docker.service docker.socket <span style="color:#75715e"># [tl! .cmd:1]</span> </span></span><span style="display:flex;"><span>sudo rm /var/run/docker.sock </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>dockerd-rootless-setuptool.sh install <span style="color:#75715e"># [tl! .cmd]</span> </span></span></code></pre></div><p>Next, I enable and start the service in the user context, and I enable &quot;linger&quot; for the <code>github</code> user so that its systemd instance can continue to function even while the user is not logged in:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>systemctl --user enable --now docker <span style="color:#75715e"># [tl! .cmd:1]</span> </span></span><span style="display:flex;"><span>sudo loginctl enable-linger <span style="color:#66d9ef">$(</span>whoami<span style="color:#66d9ef">)</span> </span></span></code></pre></div><p>That should take care of setting up Docker, and I can quickly confirm by spawning the usual <code>hello-world</code> container:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>docker run hello-world <span style="color:#75715e"># [tl! .cmd]</span> </span></span><span style="display:flex;"><span>Unable to find image <span style="color:#e6db74">&#39;hello-world:latest&#39;</span> locally <span style="color:#75715e"># [tl! .nocopy:25]</span> </span></span><span style="display:flex;"><span>latest: Pulling from library/hello-world </span></span><span style="display:flex;"><span>c1ec31eb5944: Pull complete </span></span><span style="display:flex;"><span>Digest: sha256:1408fec50309afee38f3535383f5b09419e6dc0925bc69891e79d84cc4cdcec6 </span></span><span style="display:flex;"><span>Status: Downloaded newer image <span style="color:#66d9ef">for</span> hello-world:latest </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>Hello from Docker! </span></span><span style="display:flex;"><span>This message shows that your installation appears to be working correctly. </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>To generate this message, Docker took the following steps: </span></span><span style="display:flex;"><span> 1. The Docker client contacted the Docker daemon. </span></span><span style="display:flex;"><span> 2. The Docker daemon pulled the <span style="color:#e6db74">&#34;hello-world&#34;</span> image from the Docker Hub. </span></span><span style="display:flex;"><span> <span style="color:#f92672">(</span>amd64<span style="color:#f92672">)</span> </span></span><span style="display:flex;"><span> 3. The Docker daemon created a new container from that image which runs the </span></span><span style="display:flex;"><span> executable that produces the output you are currently reading. </span></span><span style="display:flex;"><span> 4. The Docker daemon streamed that output to the Docker client, which sent it </span></span><span style="display:flex;"><span> to your terminal. </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>To try something more ambitious, you can run an Ubuntu container with: </span></span><span style="display:flex;"><span> $ docker run -it ubuntu bash </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>Share images, automate workflows, and more with a free Docker ID: </span></span><span style="display:flex;"><span> https://hub.docker.com/ </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>For more examples and ideas, visit: </span></span><span style="display:flex;"><span> https://docs.docker.com/get-started/ </span></span></code></pre></div><p>So the Docker piece is sorted; now for setting up the runner.</p> <h4 id="installconfigure-runner"> Install/Configure Runner <a class="hlink" href="#installconfigure-runner"><i class="fa-solid fa-link"></i></a> </h4><p>I know I've been talking about a singular runner, but I'm actually seting up multiple instances of the runner on the same host to allow running jobs in parallel. I could probably support four simultaneous builds in my homelab but I'll settle two runners for now (after all, I only have two build flavors so far anyway).</p> <p>Each runner instance needs its own directory so I create those under <code>/opt/github/</code>:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>sudo mkdir -p /opt/github/runner<span style="color:#f92672">{</span>1..2<span style="color:#f92672">}</span> <span style="color:#75715e"># [tl! .cmd:2]</span> </span></span><span style="display:flex;"><span>sudo chown -R github:github /opt/github </span></span><span style="display:flex;"><span>cd /opt/github </span></span></code></pre></div><p>And then I download the <a href="https://github.com/actions/runner/releases" rel="external">latest runner package↗</a>:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>curl -O -L https://github.com/actions/runner/releases/download/v2.317.0/actions-runner-linux-x64-2.317.0.tar.gz <span style="color:#75715e"># [tl! .cmd]</span> </span></span></code></pre></div><p>For each runner, I:</p> <ul> <li>Extract the runner software into the designated directory and <code>cd</code> into it: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>tar xzf ./actions-runner-linux-x64-2.317.0.tar.gz --directory<span style="color:#f92672">=</span>runner1 <span style="color:#75715e"># [tl! .cmd:1]</span> </span></span><span style="display:flex;"><span>cd runner1 </span></span></code></pre></div></li> <li>Go to my private GitHub repo, navigate to <strong>Settings &gt; Actions &gt; Runners</strong>, and click the big friendly <strong>New self-hosted runner</strong> button at the top-right of the page. All I really need from that is the token which appears in the <strong>Configure</strong> section. Once I have that token, I...</li> <li>Run the configuration script, accepting the defaults for every prompt <em>except</em> for the runner name, which must be unique within the repository (so <code>runner1</code>, <code>runner2</code>, so on): <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>./config.sh <span style="color:#ae81ff">\ </span><span style="color:#75715e"># [tl! **:2 .cmd]</span> </span></span><span style="display:flex;"><span> --url https://github.com/<span style="color:#f92672">[</span>GITHUB_USERNAME<span style="color:#f92672">]</span>/<span style="color:#f92672">[</span>GITHUB_REPO<span style="color:#f92672">]</span> <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> --token <span style="color:#f92672">[</span>TOKEN<span style="color:#f92672">]</span> <span style="color:#75715e"># [tl! .nocopy:1,35]</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>-------------------------------------------------------------------------------- </span></span><span style="display:flex;"><span>| ____ _ _ _ _ _ _ _ _ | </span></span><span style="display:flex;"><span>| / ___<span style="color:#f92672">(</span>_<span style="color:#f92672">)</span> |_| | | |_ _| |__ / <span style="color:#ae81ff">\ </span> ___| |_<span style="color:#f92672">(</span>_<span style="color:#f92672">)</span> ___ _ __ ___ | </span></span><span style="display:flex;"><span>| | | _| | __| |_| | | | | <span style="color:#e6db74">&#39;_ \ / _ \ / __| __| |/ _ \| &#39;</span>_ <span style="color:#ae81ff">\/</span> __| | </span></span><span style="display:flex;"><span>| | |_| | | |_| _ | |_| | |_<span style="color:#f92672">)</span> | / ___ <span style="color:#ae81ff">\ </span><span style="color:#f92672">(</span>__| |_| | <span style="color:#f92672">(</span>_<span style="color:#f92672">)</span> | | | <span style="color:#ae81ff">\_</span>_ <span style="color:#ae81ff">\ </span> | </span></span><span style="display:flex;"><span>| <span style="color:#ae81ff">\_</span>___|_|<span style="color:#ae81ff">\_</span>_|_| |_|<span style="color:#ae81ff">\_</span>_,_|_.__/ /_/ <span style="color:#ae81ff">\_\_</span>__|<span style="color:#ae81ff">\_</span>_|_|<span style="color:#ae81ff">\_</span>__/|_| |_|___/ | </span></span><span style="display:flex;"><span>| | </span></span><span style="display:flex;"><span>| Self-hosted runner registration | </span></span><span style="display:flex;"><span>| | </span></span><span style="display:flex;"><span>-------------------------------------------------------------------------------- </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># Authentication</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>√ Connected to GitHub </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># Runner Registration</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>Enter the name of the runner group to add this runner to: <span style="color:#f92672">[</span>press Enter <span style="color:#66d9ef">for</span> Default<span style="color:#f92672">]</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>Enter the name of runner: <span style="color:#f92672">[</span>press Enter <span style="color:#66d9ef">for</span> runner<span style="color:#f92672">]</span> runner1 <span style="color:#75715e"># [tl! ** ~~]</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>This runner will have the following labels: <span style="color:#e6db74">&#39;self-hosted&#39;</span>, <span style="color:#e6db74">&#39;Linux&#39;</span>, <span style="color:#e6db74">&#39;X64&#39;</span> </span></span><span style="display:flex;"><span>Enter any additional labels <span style="color:#f92672">(</span>ex. label-1,label-2<span style="color:#f92672">)</span>: <span style="color:#f92672">[</span>press Enter to skip<span style="color:#f92672">]</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>√ Runner successfully added </span></span><span style="display:flex;"><span>√ Runner connection is good </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># Runner settings</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>Enter name of work folder: <span style="color:#f92672">[</span>press Enter <span style="color:#66d9ef">for</span> _work<span style="color:#f92672">]</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>√ Settings Saved. </span></span></code></pre></div></li> <li>Use the <code>svc.sh</code> script to install it as a user service, and start it running as the <code>github</code> user: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>sudo ./svc.sh install <span style="color:#66d9ef">$(</span>whoami<span style="color:#66d9ef">)</span> <span style="color:#75715e"># [tl! .cmd:1]</span> </span></span><span style="display:flex;"><span>sudo ./svc.sh start <span style="color:#66d9ef">$(</span>whoami<span style="color:#66d9ef">)</span> </span></span></code></pre></div></li> </ul> <p>Once all of the runner instances are configured I can remove the <code>github</code> user from the <code>sudo</code> group:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>sudo deluser github sudo <span style="color:#75715e"># [tl! .cmd]</span> </span></span></code></pre></div><p>And I can see that my new runners are successfully connected to my <em>private</em> GitHub repo: <img src="https://runtimeterror.dev/automate-packer-builds-github-actions/new-runners.png" alt="GitHub settings showing two self-hosted runners with status &quot;Idle&quot;"></p> <p>I now have a place to execute the Packer builds, I just need to tell the runner how to do that. And that's means it's time to talk about the...</p> <h3 id="github-actions-workflow"> GitHub Actions Workflow <a class="hlink" href="#github-actions-workflow"><i class="fa-solid fa-link"></i></a> </h3><p>My solution for this consists of a Github Actions workflow which calls a custom action to spawn a Docker container and do the work. Let's start with the innermost component (the Docker image) and work out from there.</p> <h4 id="docker-image"> Docker Image <a class="hlink" href="#docker-image"><i class="fa-solid fa-link"></i></a> </h4><p>I'm using a customized Docker image consisting of Packer and associated tools with the addition of the <a href="https://runtimeterror.dev/building-proxmox-templates-packer/#wrapper-script">wrapper script</a> that I used for local builds. That image will be integrated with a custom action called <code>packerbuild</code>.</p> <p>So I'll create a folder to hold my new action (and Dockerfile):</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>mkdir -p .github/actions/packerbuild <span style="color:#75715e"># [tl! .cmd]</span> </span></span></code></pre></div><p>I don't want to maintain two copies of the <code>build.sh</code> script, so I move it into this new folder and create a symlink to it back at the top of the repo:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>mv build.sh .github/actions/packerbuild/ <span style="color:#75715e"># [tl! .cmd:1]</span> </span></span><span style="display:flex;"><span>ln -s .github/actions/packerbuild/build.sh build.sh </span></span></code></pre></div><p>That way I can easily load the script into the Docker image while also having it available for running on-demand local builds as needed.</p> <p>And as a quick reminder, that <code>build.sh</code> script accepts a single argument to specify what build to produce and then fires off the appropriate Packer commands:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># Run a single packer build</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># Specify the build as an argument to the script. Ex:</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># ./build.sh ubuntu2204</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> $# -ne <span style="color:#ae81ff">1</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;&#34;&#34; </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Syntax: </span>$0<span style="color:#e6db74"> [BUILD] </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Where [BUILD] is one of the supported OS builds: </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">ubuntu2204 ubuntu2404 </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span> </span></span><span style="display:flex;"><span> exit <span style="color:#ae81ff">1</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> ! <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>VAULT_TOKEN+x<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">#shellcheck disable=SC1091</span> </span></span><span style="display:flex;"><span> source vault-env.sh <span style="color:#f92672">||</span> <span style="color:#f92672">(</span> echo <span style="color:#e6db74">&#34;No Vault config found&#34;</span>; exit <span style="color:#ae81ff">1</span> <span style="color:#f92672">)</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>build_name<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>1,,<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> </span></span><span style="display:flex;"><span>build_path<span style="color:#f92672">=</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">case</span> $build_name in </span></span><span style="display:flex;"><span> ubuntu2204<span style="color:#f92672">)</span> </span></span><span style="display:flex;"><span> build_path<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;builds/linux/ubuntu/22-04-lts/&#34;</span> </span></span><span style="display:flex;"><span> ;; </span></span><span style="display:flex;"><span> ubuntu2404<span style="color:#f92672">)</span> </span></span><span style="display:flex;"><span> build_path<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;builds/linux/ubuntu/24-04-lts/&#34;</span> </span></span><span style="display:flex;"><span> ;; </span></span><span style="display:flex;"><span> *<span style="color:#f92672">)</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;Unknown build; exiting...&#34;</span> </span></span><span style="display:flex;"><span> exit <span style="color:#ae81ff">1</span> </span></span><span style="display:flex;"><span> ;; </span></span><span style="display:flex;"><span><span style="color:#66d9ef">esac</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>packer init <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>build_path<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> </span></span><span style="display:flex;"><span>packer build -on-error<span style="color:#f92672">=</span>cleanup -force <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>build_path<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> </span></span></code></pre></div><p>I use the following <code>Dockerfile</code> to create the environment in which the build will be executed:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-Dockerfile" data-lang="Dockerfile"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span><span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> alpine:3.20</span><span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PACKER_VERSION<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>.10.3<span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apk --no-cache upgrade <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> <span style="color:#f92672">&amp;&amp;</span> apk add --no-cache <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> bash <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> curl <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> git <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> openssl <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> wget <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> xorriso<span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ADD</span> https://releases.hashicorp.com/packer/<span style="color:#e6db74">${</span>PACKER_VERSION<span style="color:#e6db74">}</span>/packer_<span style="color:#e6db74">${</span>PACKER_VERSION<span style="color:#e6db74">}</span>_linux_amd64.zip ./<span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ADD</span> https://releases.hashicorp.com/packer/<span style="color:#e6db74">${</span>PACKER_VERSION<span style="color:#e6db74">}</span>/packer_<span style="color:#e6db74">${</span>PACKER_VERSION<span style="color:#e6db74">}</span>_SHA256SUMS ./<span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> sed -i <span style="color:#e6db74">&#39;/.*linux_amd64.zip/!d&#39;</span> packer_<span style="color:#e6db74">${</span>PACKER_VERSION<span style="color:#e6db74">}</span>_SHA256SUMS <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> <span style="color:#f92672">&amp;&amp;</span> sha256sum -c packer_<span style="color:#e6db74">${</span>PACKER_VERSION<span style="color:#e6db74">}</span>_SHA256SUMS <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> <span style="color:#f92672">&amp;&amp;</span> unzip packer_<span style="color:#e6db74">${</span>PACKER_VERSION<span style="color:#e6db74">}</span>_linux_amd64.zip -d /bin <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> <span style="color:#f92672">&amp;&amp;</span> rm -f packer_<span style="color:#e6db74">${</span>PACKER_VERSION<span style="color:#e6db74">}</span>_linux_amd64.zip packer_<span style="color:#e6db74">${</span>PACKER_VERSION<span style="color:#e6db74">}</span>_SHA256SUMS<span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> build.sh /bin/build.sh<span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> chmod +x /bin/build.sh<span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"> </span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENTRYPOINT</span> [<span style="color:#e6db74">&#34;/bin/build.sh&#34;</span>]<span style="color:#960050;background-color:#1e0010"> </span></span></span></code></pre></div><p>It starts with a minimal <code>alpine</code> base image and installs a few common packages (and <code>xorriso</code> to support the creation of ISO images). It then downloads the indicated version of the Packer installer and extracts it to <code>/bin/</code>. Finally it copies the <code>build.sh</code> script into the image and sets it as the <code>ENTRYPOINT</code>.</p> <h4 id="custom-action"> Custom Action <a class="hlink" href="#custom-action"><i class="fa-solid fa-link"></i></a> </h4><p>Turning this Docker image into an action requires just a smidge of YAML to describe how to interact with the image.</p> <p>Behold, <code>.github/actions/packerbuild/action.yml</code>:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#e6db74">&#39;Execute Packer Build&#39;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">description</span>: <span style="color:#e6db74">&#39;Performs a Packer build&#39;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">inputs</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">build-flavor</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">description</span>: <span style="color:#e6db74">&#39;The build to execute&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">runs</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">using</span>: <span style="color:#e6db74">&#39;docker&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#e6db74">&#39;Dockerfile&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">args</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">${{ inputs.build-flavor }}</span> </span></span></code></pre></div><p>As you can see, the action expects (nay, requires!) a <code>build-flavor</code> input to line up with <code>build.sh</code>'s expected parameter. The action will run in Docker using the image defined in the local <code>Dockerfile</code>, and will pass <code>${{ inputs.build-flavor }}</code> as the sole argument to that image.</p> <p>Alright, let's tie it all together with the automation workflow now.</p> <h4 id="the-workflow"> The Workflow <a class="hlink" href="#the-workflow"><i class="fa-solid fa-link"></i></a> </h4><p>The workflow is defined in <code>.github/workflows/build.yml</code>. It starts simply enough with a name and an explanation of when the workflow should be executed.</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build VM Templates</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">workflow_dispatch</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">schedule</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f92672">cron</span>: <span style="color:#e6db74">&#39;0 8 * * 1&#39;</span> </span></span></code></pre></div><p><code>workflow_dispatch</code> sets it so I can manually execute the workflow from the GitHub Actions UI (for testing / as a treat), and the <code>cron</code> schedule configures the workflow to run automatically every Monday at 8:00 AM (UTC).</p> <p>Rather than rely on an environment file (ew), I'm securely storing the <code>VAULT_ADDR</code> and <code>VAULT_TOKEN</code> values in GitHub <a href="https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions" rel="external">repository secrets↗</a>. So I introduce those values into the workflow like so:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true, &#34;lineNumbersStart&#34;:8}</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">env</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">VAULT_ADDR</span>: <span style="color:#ae81ff">${{ secrets.VAULT_ADDR }}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">VAULT_TOKEN</span>: <span style="color:#ae81ff">${{ secrets.VAULT_TOKEN }}</span> </span></span></code></pre></div><p>When I did the <a href="https://runtimeterror.dev/building-proxmox-templates-packer/#vault-configuration">Vault setup</a>, I created the token with a <code>period</code> of <code>336</code> hours; that means that the token will only remain valid as long as it gets renewed at least once every two weeks. So I start the <code>jobs:</code> block with a simple call to <a href="https://developer.hashicorp.com/vault/api-docs/auth/token#renew-a-token-self" rel="external">Vault's REST API↗</a> to renew the token before each run:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true, &#34;lineNumbersStart&#34;:12}</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">prepare</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Prepare</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">self-hosted</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">steps</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Renew Vault Token</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: |<span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> curl -s --header &#34;X-Vault-Token:${VAULT_TOKEN}&#34; \ </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --request POST &#34;${VAULT_ADDR}v1/auth/token/renew-self&#34; | grep -q auth</span> </span></span></code></pre></div><p>Assuming that token is renewed successfully, the Build job uses a <a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixinclude" rel="external">matrix strategy↗</a> to enumerate the <code>build-flavor</code>s that will need to be built. All of the following steps will be repeated for each flavor.</p> <p>And the first step is to simply check out the GitHub repo so that the runner has all the latest code.</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true, &#34;lineNumbersStart&#34;:22}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">builds</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">needs</span>: <span style="color:#ae81ff">prepare</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">self-hosted</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">strategy</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">matrix</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">build-flavor</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">ubuntu2204</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">ubuntu2404</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">steps</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Checkout</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span> </span></span></code></pre></div><p>To get the runner to interact with the rootless Docker setup we'll need to export the <code>DOCKER_HOST</code> variable and point it to the Docker socket registered by the user... which first means obtaining the UID of that user and echoing it to the special <code>$GITHUB_OUTPUT</code> variable so it can be passed to the next step:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true, &#34;lineNumbersStart&#34;:34}</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Get UID of Github user</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">runner_uid</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: |<span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> echo &#34;gh_uid=$(id -u)&#34; &gt;&gt; &#34;$GITHUB_OUTPUT&#34;</span> </span></span></code></pre></div><p>And now, finally, for the actual build. The <code>Build template</code> step calls the <code>.github/actions/packerbuild</code> custom action, sets the <code>DOCKER_HOST</code> value to the location of <code>docker.sock</code> (using the UID obtained earlier) so the runner will know how to interact with rootless Docker, and passes along the <code>build-flavor</code> from the matrix to influence which template will be created.</p> <p>If it fails for some reason, the <code>Retry on failure</code> step will try again, just in case it was a transient glitch like a network error or a hung process.</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true, &#34;lineNumbersStart&#34;:38}</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build template</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">build</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">./.github/actions/packerbuild</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">timeout-minutes</span>: <span style="color:#ae81ff">90</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">env</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">DOCKER_HOST</span>: <span style="color:#ae81ff">unix:///run/user/${{ steps.runner_uid.outputs.gh_uid }}/docker.sock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">with</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">build-flavor</span>: <span style="color:#ae81ff">${{ matrix.build-flavor }}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">continue-on-error</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Retry on failure</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">retry</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">if</span>: <span style="color:#ae81ff">steps.build.outcome == &#39;failure&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">./.github/actions/packerbuild</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">timeout-minutes</span>: <span style="color:#ae81ff">90</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">env</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">DOCKER_HOST</span>: <span style="color:#ae81ff">unix:///run/user/${{ steps.runner_uid.outputs.gh_uid }}/docker.sock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">with</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">build-flavor</span>: <span style="color:#ae81ff">${{ matrix.build-flavor }}</span> </span></span></code></pre></div><p>Here's the complete <code>.github/workflows/build.yml</code>, all in one code block:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build VM Templates</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">workflow_dispatch</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">schedule</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f92672">cron</span>: <span style="color:#e6db74">&#39;0 8 * * 1&#39;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#f92672">env</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">VAULT_ADDR</span>: <span style="color:#ae81ff">${{ secrets.VAULT_ADDR }}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">VAULT_TOKEN</span>: <span style="color:#ae81ff">${{ secrets.VAULT_TOKEN }}</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">prepare</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Prepare</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">self-hosted</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">steps</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Renew Vault Token</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: |<span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> curl -s --header &#34;X-Vault-Token:${VAULT_TOKEN}&#34; \ </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --request POST &#34;${VAULT_ADDR}v1/auth/token/renew-self&#34; | grep -q auth</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">builds</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">needs</span>: <span style="color:#ae81ff">prepare</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">self-hosted</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">strategy</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">matrix</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">build-flavor</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">ubuntu2204</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">ubuntu2404</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">steps</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Checkout</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Get UID of Github user</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">runner_uid</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: |<span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> echo &#34;gh_uid=$(id -u)&#34; &gt;&gt; &#34;$GITHUB_OUTPUT&#34;</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build template</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">build</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">./.github/actions/packerbuild</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">timeout-minutes</span>: <span style="color:#ae81ff">90</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">env</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">DOCKER_HOST</span>: <span style="color:#ae81ff">unix:///run/user/${{ steps.runner_uid.outputs.gh_uid }}/docker.sock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">with</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">build-flavor</span>: <span style="color:#ae81ff">${{ matrix.build-flavor }}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">continue-on-error</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Retry on failure</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">retry</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">if</span>: <span style="color:#ae81ff">steps.build.outcome == &#39;failure&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">./.github/actions/packerbuild</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">timeout-minutes</span>: <span style="color:#ae81ff">90</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">env</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">DOCKER_HOST</span>: <span style="color:#ae81ff">unix:///run/user/${{ steps.runner_uid.outputs.gh_uid }}/docker.sock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">with</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">build-flavor</span>: <span style="color:#ae81ff">${{ matrix.build-flavor }}</span> </span></span></code></pre></div><h3 id="your-templates-are-served"> Your Templates Are Served <a class="hlink" href="#your-templates-are-served"><i class="fa-solid fa-link"></i></a> </h3><p>All that's left at this point is to <code>git commit</code> and <code>git push</code> this to my <em>private</em> repo. I can then visit the repo on the web, go to the <strong>Actions</strong> tab, select the new <strong>Build VM Templates</strong> workflow on the left, and click the <strong>Run workflow</strong> button. That fires off the build, and I can check back a few minutes later to confirm that it completed successfully:</p> <p><img src="https://runtimeterror.dev/automate-packer-builds-github-actions/successful-action-run.png" alt="GitHub interface showing that the manually-triggered workflow successfully completed"></p> <p>And I can also consult with my Proxmox host and confirm that the new VM templates were indeed created:</p> <p><img src="https://runtimeterror.dev/automate-packer-builds-github-actions/new-proxmox-templates.png" alt="Proxmox interface showing a VM template named Ubuntu2204 with a note indicating it was recently built by Packer"></p> <p>For future builds, I don't have to actually do anything at all. GitHub will automatically trigger this workflow every Monday morning so my templates will never be more than a week out-of-date. Pretty slick, right?</p> <p>You can check out my <em>public</em> repo at <a href="https://github.com/jbowdre/packer-proxmox-templates/" rel="external">github.com/jbowdre/packer-proxmox-templates/↗</a> to explore the full setup - and to follow along as I add support for additional OS flavors.</p> </description> </item> <item> <title>Building Proxmox Templates with Packer</title> <link>https://runtimeterror.dev/building-proxmox-templates-packer/</link> <pubDate>Sun, 21 Jul 2024 00:36:16 +0000</pubDate> <dc:creator>John Bowdre</dc:creator> <category>Homelab</category> <category>Iac</category> <category>Linux</category> <category>Packer</category> <category>Proxmox</category> <category>Tailscale</category> <category>Vault</category> <guid>https://runtimeterror.dev/building-proxmox-templates-packer/</guid><description><p>I've been <a href="https://runtimeterror.dev/ditching-vsphere-for-proxmox/">using Proxmox</a> in my <a href="https://runtimeterror.dev/homelab/">homelab</a> for a while now, and I recently expanded the environment with two HP Elite Mini 800 G9 computers. It was time to start automating the process of building and maintaining my VM templates. I already had functional <a href="https://github.com/jbowdre/packer-vsphere-templates" rel="external">Packer templates for VMware↗</a> so I used that as a starting point for the <a href="https://github.com/jbowdre/packer-proxmox-templates" rel="external">Proxmox builds↗</a>. So far, I've only ported over the Ubuntu builds; I'm telling myself I'll get the rest moved over after <em>finally</em> publishing this post.</p> <p>Once I got the builds working locally, I explored how to automate them. I set up a GitHub Actions workflow and a rootless runner to perform the builds for me. I wrote up some notes on that part of the process <a href="https://runtimeterror.dev/automate-packer-builds-github-actions/">here</a>, but first, let's run through how I set up Packer. That will be plenty to chew on for now.</p> <p>This post will cover a lot of the Packer implementation details but may gloss over some general setup steps; you'll need at least a passing familiarity with <a href="https://www.packer.io/" rel="external">Packer↗</a> and <a href="https://www.vaultproject.io/" rel="external">Vault↗</a> to take this on.</p> <h3 id="component-overview"> Component Overview <a class="hlink" href="#component-overview"><i class="fa-solid fa-link"></i></a> </h3><p>There are several important parts to this setup, so let's start by quickly running through those:</p> <ul> <li>a <strong>Proxmox host</strong> to serve the virtual infrastructure and provide compute for the new templates,</li> <li>a <strong>Vault instance</strong> running in a container in the lab to hold the secrets needed for the builds,</li> <li>and some <strong>Packer content</strong> for actually building the templates.</li> </ul> <h3 id="proxmox-setup"> Proxmox Setup <a class="hlink" href="#proxmox-setup"><i class="fa-solid fa-link"></i></a> </h3><p>The only configuration I did on the Proxmox side was to <a href="https://pve.proxmox.com/pve-docs/chapter-pveum.html#pveum_users" rel="external">create a user account↗</a> that Packer could use. I called it <code>packer</code> but didn't set a password for it. Instead, I set up an <a href="https://pve.proxmox.com/pve-docs/chapter-pveum.html#pveum_tokens" rel="external">API token↗</a> for that account, making sure to <strong>uncheck</strong> the &quot;Privilege Separation&quot; box so that the token would inherit the same permissions as the user itself.</p> <p><img src="https://runtimeterror.dev/building-proxmox-templates-packer/proxmox-token.png" alt="Creating an API token"></p> <p>To use the token, I needed the ID (in the form <code>USERNAME@REALM!TOKENNAME</code>) and the UUID-looking secret, which is only displayed once, so I made sure to record it in a safe place.</p> <p>Speaking of privileges, the <a href="https://developer.hashicorp.com/packer/integrations/hashicorp/proxmox/latest/components/builder/iso" rel="external">Proxmox ISO integration documentation↗</a> doesn't offer any details on the minimum required permissions, and none of my attempts worked until I eventually assigned the Administrator role to the <code>packer</code> user. I plan on doing more testing to narrow the scope before running this in production, but this will do for my homelab purposes.</p> <p>Otherwise, I just needed to figure out the details like which network bridge, ISO storage, and VM storage the Packer-built VMs should use.</p> <h3 id="vault-configuration"> Vault Configuration <a class="hlink" href="#vault-configuration"><i class="fa-solid fa-link"></i></a> </h3><p>I use <a href="https://github.com/hashicorp/vault" rel="external">Vault↗</a> to hold the configuration details for the template builds - not just traditional secrets like usernames and passwords, but basically every environment-specific setting as well. This approach lets others use my Packer code without having to change much (if any) of it; every value that I expect to change between environments is retrieved from Vault at runtime.</p> <p>Because this is just a homelab, I'm using <a href="https://hub.docker.com/r/hashicorp/vault" rel="external">Vault in Docker↗</a>, and I'm making it available within my tailnet with <a href="https://runtimeterror.dev/tailscale-serve-docker-compose-sidecar/">Tailscale Serve</a> using the following <code>docker-compose.yaml</code></p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">services</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">tailscale</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">tailscale/tailscale:latest</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">vault-tailscaled</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">environment</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_AUTHKEY</span>: <span style="color:#ae81ff">${TS_AUTHKEY:?err}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_HOSTNAME</span>: <span style="color:#ae81ff">vault</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_STATE_DIR</span>: <span style="color:#e6db74">&#34;/var/lib/tailscale/&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">TS_SERVE_CONFIG</span>: <span style="color:#ae81ff">/config/serve-config.json</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">./ts_data:/var/lib/tailscale/</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">./serve-config.json:/config/serve-config.json</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">vault</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">hashicorp/vault</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">vault</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">environment</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">VAULT_ADDR</span>: <span style="color:#e6db74">&#39;https://0.0.0.0:8200&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">cap_add</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">IPC_LOCK</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">./data:/vault/data</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">./config:/vault/config</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">./log:/vault/log</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">command</span>: <span style="color:#ae81ff">vault server -config=/vault/config/vault.hcl</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">network_mode</span>: <span style="color:#e6db74">&#34;service:tailscale&#34;</span> </span></span></code></pre></div><p>I use the following <code>./config/vault.hcl</code> to set the Vault server configuration:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span>ui <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">listener</span> <span style="color:#e6db74">&#34;tcp&#34;</span> { </span></span><span style="display:flex;"><span> address <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0.0.0.0:8200&#34;</span> </span></span><span style="display:flex;"><span> tls_disable <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">storage</span> <span style="color:#e6db74">&#34;file&#34;</span> { </span></span><span style="display:flex;"><span> path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/vault/data&#34;</span> </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>And this <code>./serve-config.json</code> to tell Tailscale that it should proxy the Vault container's port <code>8200</code> and make it available on my tailnet at <code>https://vault.tailnet-name.ts.net/</code>:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-json" data-lang="json"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">#</span> <span style="color:#960050;background-color:#1e0010">torchlight!</span> {<span style="color:#f92672">&#34;lineNumbers&#34;</span>:<span style="color:#66d9ef">true</span>} </span></span><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;TCP&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;443&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;HTTPS&#34;</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;Web&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;vault.tailnet-name.ts.net:443&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;Handlers&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;/&#34;</span>: { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;Proxy&#34;</span>: <span style="color:#e6db74">&#34;http://127.0.0.1:8200&#34;</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>After performing the initial Vault setup, I then created a <a href="https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2" rel="external">kv-v2↗</a> secrets engine for Packer to use:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>vault secrets enable -path<span style="color:#f92672">=</span>packer kv-v2 <span style="color:#75715e"># [tl! .cmd]</span> </span></span><span style="display:flex;"><span>Success! Enabled the kv-v2 secrets engine at: packer/ <span style="color:#75715e"># [tl! .nocopy]</span> </span></span></code></pre></div><p>I defined a <a href="https://developer.hashicorp.com/vault/docs/concepts/policies" rel="external">policy↗</a> which will grant the bearer read-only access to the data stored in the <code>packer</code> secrets as well as the ability to create and update its own token:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>cat <span style="color:#e6db74">&lt;&lt; EOF | vault policy write packer - </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">path &#34;packer/*&#34; { </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> capabilities = [&#34;read&#34;, &#34;list&#34;] </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">} </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">path &#34;auth/token/renew-self&#34; { </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> capabilities = [&#34;update&#34;] </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">} </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">path &#34;auth/token/create&#34; { </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> capabilities = [&#34;create&#34;, &#34;update&#34;] </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">} </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">EOF</span> <span style="color:#75715e"># [tl! .cmd:-12,1]</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>Success! Uploaded policy: packer2 <span style="color:#75715e"># [tl! .nocopy]</span> </span></span></code></pre></div><p>Now I just need to create a token attached to the policy:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>vault token create -policy<span style="color:#f92672">=</span>packer -no-default-policy <span style="color:#ae81ff">\ </span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> -orphan -ttl<span style="color:#f92672">=</span>4h -period<span style="color:#f92672">=</span>336h -display-name<span style="color:#f92672">=</span>packer <span style="color:#75715e"># [tl! .cmd:-1,1 ]</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>Key Value <span style="color:#75715e"># [tl! .nocopy:8]</span> </span></span><span style="display:flex;"><span>--- ----- </span></span><span style="display:flex;"><span>token hvs.CAES<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>GSFQ </span></span><span style="display:flex;"><span>token_accessor aleV<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>xu5I </span></span><span style="display:flex;"><span>token_duration 336h </span></span><span style="display:flex;"><span>token_renewable true </span></span><span style="display:flex;"><span>token_policies <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;packer&#34;</span><span style="color:#f92672">]</span> </span></span><span style="display:flex;"><span>identity_policies <span style="color:#f92672">[]</span> </span></span><span style="display:flex;"><span>policies <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;packer&#34;</span><span style="color:#f92672">]</span> </span></span></code></pre></div><p>The token will only be displayed this once so I make sure to copy it somewhere safe.</p> <p>Within the <code>packer</code> secrets engine, I have two secrets which each have a number of subkeys.</p> <p><code>proxmox</code> contains values related to the Proxmox environment:</p> <table> <thead> <tr> <th>Key</th> <th>Example value</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td><code>api_url</code></td> <td><code>https://prox.tailnet-name.ts.net/api2/json/</code></td> <td>URL to the Proxmox API</td> </tr> <tr> <td><code>insecure_connection</code></td> <td><code>true</code></td> <td>set to <code>false</code> if your Proxmox host has a valid certificate</td> </tr> <tr> <td><code>iso_path</code></td> <td><code>local:iso</code></td> <td>path for (existing) ISO storage</td> </tr> <tr> <td><code>iso_storage_pool</code></td> <td><code>local</code></td> <td>pool for storing created/uploaded ISOs</td> </tr> <tr> <td><code>network_bridge</code></td> <td><code>vmbr0</code></td> <td>bridge the VM's NIC will be attached to</td> </tr> <tr> <td><code>node</code></td> <td><code>proxmox1</code></td> <td>node name where the VM will be built</td> </tr> <tr> <td><code>token_id</code></td> <td><code>packer@pve!packer</code></td> <td>ID for an <a href="https://pve.proxmox.com/wiki/User_Management#pveum_tokens" rel="external">API token↗</a>, in the form <code>USERNAME@REALM!TOKENNAME</code></td> </tr> <tr> <td><code>token_secret</code></td> <td><code>3fc69f[...]d2077eda</code></td> <td>secret key for the token</td> </tr> <tr> <td><code>vm_storage_pool</code></td> <td><code>zfs-pool</code></td> <td>storage pool where the VM will be created</td> </tr> </tbody> </table> <p><code>linux</code> holds values for the created VM template(s)</p> <table> <thead> <tr> <th>Key</th> <th>Example value</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td><code>bootloader_password</code></td> <td><code>bootplease</code></td> <td>Grub bootloader password to set</td> </tr> <tr> <td><code>password_hash</code></td> <td><code>$6$rounds=4096$NltiNLKi[...]a7Shax41</code></td> <td>hash of the build account's password (example generated with <code>mkpasswd -m sha512crypt -R 4096</code>)</td> </tr> <tr> <td><code>public_key</code></td> <td><code>ssh-ed25519 AAAAC3NzaC1[...]lXLUI5I40 admin@example.com</code></td> <td>SSH public key for the user</td> </tr> <tr> <td><code>username</code></td> <td><code>admin</code></td> <td>build account username</td> </tr> </tbody> </table> <h3 id="packer-content"> Packer Content <a class="hlink" href="#packer-content"><i class="fa-solid fa-link"></i></a> </h3><p>The layout of my <a href="https://github.com/jbowdre/packer-proxmox-templates/" rel="external">Packer Proxmox repo↗</a> looks something like this:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>. </span></span><span style="display:flex;"><span>├── builds </span></span><span style="display:flex;"><span>│ └── linux </span></span><span style="display:flex;"><span>│ └── ubuntu </span></span><span style="display:flex;"><span>│ ├── 22-04-lts </span></span><span style="display:flex;"><span>│ │ ├── data </span></span><span style="display:flex;"><span>│ │ │ ├── meta-data </span></span><span style="display:flex;"><span>│ │ │ └── user-data.pkrtpl.hcl </span></span><span style="display:flex;"><span>│ │ ├── hardening.sh </span></span><span style="display:flex;"><span>│ │ ├── linux-server.auto.pkrvars.hcl </span></span><span style="display:flex;"><span>│ │ ├── linux-server.pkr.hcl </span></span><span style="display:flex;"><span>│ │ └── variables.pkr.hcl </span></span><span style="display:flex;"><span>│ └── 24-04-lts [tl! collapse:7 ] </span></span><span style="display:flex;"><span>│ ├── data </span></span><span style="display:flex;"><span>│ │ ├── meta-data </span></span><span style="display:flex;"><span>│ │ └── user-data.pkrtpl.hcl </span></span><span style="display:flex;"><span>│ ├── hardening.sh </span></span><span style="display:flex;"><span>│ ├── linux-server.auto.pkrvars.hcl </span></span><span style="display:flex;"><span>│ ├── linux-server.pkr.hcl </span></span><span style="display:flex;"><span>│ └── variables.pkr.hcl </span></span><span style="display:flex;"><span>├── certs </span></span><span style="display:flex;"><span>├── scripts </span></span><span style="display:flex;"><span>│ └── linux [tl! collapse:16 ] </span></span><span style="display:flex;"><span>│ ├── cleanup-cloud-init.sh </span></span><span style="display:flex;"><span>│ ├── cleanup-packages.sh </span></span><span style="display:flex;"><span>│ ├── cleanup-subiquity.sh </span></span><span style="display:flex;"><span>│ ├── configure-pam_mkhomedir.sh </span></span><span style="display:flex;"><span>│ ├── configure-sshd.sh </span></span><span style="display:flex;"><span>│ ├── disable-multipathd.sh </span></span><span style="display:flex;"><span>│ ├── generalize.sh </span></span><span style="display:flex;"><span>│ ├── install-ca-certs.sh </span></span><span style="display:flex;"><span>│ ├── install-cloud-init.sh </span></span><span style="display:flex;"><span>│ ├── join-domain.sh </span></span><span style="display:flex;"><span>│ ├── persist-cloud-init-net.sh </span></span><span style="display:flex;"><span>│ ├── prune-motd.sh </span></span><span style="display:flex;"><span>│ ├── set-homedir-privacy.sh </span></span><span style="display:flex;"><span>│ ├── update-packages.sh </span></span><span style="display:flex;"><span>│ ├── wait-for-cloud-init.sh </span></span><span style="display:flex;"><span>│ └── zero-disk.sh </span></span><span style="display:flex;"><span>├── build.sh </span></span><span style="display:flex;"><span>└── vault-env.sh </span></span></code></pre></div><ul> <li><code>.github/</code> holds the actions and workflows that will perform the automated builds. I'll cover this later.</li> <li><code>builds/</code> contains subfolders for OS types (Linux or Windows (eventually)) and then separate subfolders for each flavor. <ul> <li><code>linux/ubuntu/22-04-lts/</code> holds everything related to the Ubuntu 22.04 build: <ul> <li><code>data/meta-data</code> is an empty placeholder,</li> <li><code>data/user-data.pkrtpl.hcl</code> is a template file for <code>cloud-init</code> to perform the initial install,</li> <li><code>hardening.sh</code> is a script to perform basic security hardening,</li> <li><code>variables.pkr.hcl</code> describes all the variables for the build,</li> <li><code>linux-server.auto.pkrvars.hcl</code> assigns values to each of those variables, and</li> <li><code>linux-server.pkr.hcl</code> details the steps for actually performing the build.</li> </ul> </li> </ul> </li> <li><code>certs/</code> is empty in my case but could contain CA certificates that need to be installed in the template.</li> <li><code>scripts/linux/</code> contains a variety of scripts that will be executed by Packer as part of the build.</li> <li><code>build.sh</code> is a wrapper script which helps with running the builds locally.</li> <li><code>vault-env.sh</code> exports variables for connecting to my Vault instance for use by <code>build.sh</code>.</li> </ul> <h4 id="input-variable-definitions"> Input Variable Definitions <a class="hlink" href="#input-variable-definitions"><i class="fa-solid fa-link"></i></a> </h4><p>Let's take a quick look at the variable definitions in <code>variables.pkr.hcl</code> first. All it does is define the available variables along with their type, provide a brief description about what the variable should hold or be used for, and set sane defaults for some of them.</p> <div></div><div class="notice note"> <p class="first notice-title"><span class="icon-notice baseline"></span>Input Variables and Local Variables</p><p>There are two types of variables used with Packer:</p> <ul> <li><strong><a href="https://developer.hashicorp.com/packer/docs/templates/hcl_templates/variables" rel="external">Input Variables↗</a></strong> may have defined defaults, can be overridden, but cannot be changed after that initial override. They serve as build parameters, allowing aspects of the build to be altered without having to change the source code.</li> <li><strong><a href="https://developer.hashicorp.com/packer/docs/templates/hcl_templates/locals" rel="external">Local Variables↗</a></strong> are useful for assigning a name to an expression. These expressions are evaluated at runtime and can work with input variables, other local variables, data sources, and built-in functions.</li> </ul> <p>Input variables are great for those predefined values, while local variables can be really handy for stuff that needs to be more dynamic.</p></div> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true} </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e">/* </span></span></span><span style="display:flex;"><span><span style="color:#75715e"> Ubuntu Server 22.04 LTS variables using the Packer Builder for Proxmox. </span></span></span><span style="display:flex;"><span><span style="color:#75715e">*/</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">BLOCK</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">variable</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Defines</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">input</span> <span style="color:#66d9ef">variables</span>. </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Virtual</span> <span style="color:#66d9ef">Machine</span> <span style="color:#66d9ef">Settings</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;remove_cdrom&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">bool</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Remove the virtual CD-ROM(s).&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_name&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Name of the new template to create.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_cpu_cores&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The number of virtual CPUs cores per socket. (e.g. &#39;1&#39;)&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_cpu_count&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The number of virtual CPUs. (e.g. &#39;2&#39;)&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_cpu_type&#34;</span> {<span style="color:#75715e"> # [tl! collapse:start] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The virtual machine CPU type. (e.g. &#39;host&#39;)&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_disk_size&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The size for the virtual disk (e.g. &#39;60G&#39;)&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;60G&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_bios_type&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The virtual machine BIOS type (e.g. &#39;ovmf&#39; or &#39;seabios&#39;)&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ovmf&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_os_keyboard&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The guest operating system keyboard input.&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;us&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_os_language&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The guest operating system lanugage.&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;en_US&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_os_timezone&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The guest operating system timezone.&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;UTC&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_os_type&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The guest operating system type. (e.g. &#39;l26&#39; for Linux 2.6+)&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_mem_size&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The size for the virtual memory in MB. (e.g. &#39;2048&#39;)&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_network_model&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The virtual network adapter type. (e.g. &#39;e1000&#39;, &#39;vmxnet3&#39;, or &#39;virtio&#39;)&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_scsi_controller&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The virtual SCSI controller type. (e.g. &#39;virtio-scsi-single&#39;)&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio-scsi-single&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">VM</span> <span style="color:#66d9ef">Guest</span> <span style="color:#66d9ef">Partition</span> <span style="color:#66d9ef">Sizes</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_part_audit&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Size of the /var/log/audit partition in MB.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_part_boot&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Size of the /boot partition in MB.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_part_efi&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Size of the /boot/efi partition in MB.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_part_home&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Size of the /home partition in MB.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_part_log&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Size of the /var/log partition in MB.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_part_root&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Size of the /var partition in MB. Set to 0 to consume all remaining free space.&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_part_swap&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Size of the swap partition in MB.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_part_tmp&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Size of the /tmp partition in MB.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_part_var&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Size of the /var partition in MB.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_guest_part_vartmp&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Size of the /var/tmp partition in MB.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Removable</span> <span style="color:#66d9ef">Media</span> <span style="color:#66d9ef">Settings</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;cd_label&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;CD Label&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cidata&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;iso_checksum_type&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The checksum algorithm used by the vendor. (e.g. &#39;sha256&#39;)&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;iso_checksum_value&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The checksum value provided by the vendor.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;iso_file&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The file name of the ISO image used by the vendor. (e.g. &#39;ubuntu-&lt;version&gt;-live-server-amd64.iso&#39;)&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;iso_url&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The URL source of the ISO image. (e.g. &#39;https://mirror.example.com/.../os.iso&#39;)&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Boot</span> <span style="color:#66d9ef">Settings</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_boot_command&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">list</span>(<span style="color:#66d9ef">string</span>) </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The virtual machine boot command.&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> [] </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;vm_boot_wait&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The time to wait before boot.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Communicator</span> <span style="color:#66d9ef">Settings</span> <span style="color:#66d9ef">and</span> <span style="color:#66d9ef">Credentials</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;build_remove_keys&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">bool</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;If true, Packer will attempt to remove its temporary key from ~/.ssh/authorized_keys and /root/.ssh/authorized_keys&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;communicator_insecure&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">bool</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;If true, do not check server certificate chain and host name&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;communicator_port&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The port for the communicator protocol.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;communicator_ssl&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">bool</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;If true, use SSL&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;communicator_timeout&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;The timeout for the communicator protocol.&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Provisioner</span> <span style="color:#66d9ef">Settings</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;cloud_init_apt_packages&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">list</span>(<span style="color:#66d9ef">string</span>) </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;A list of apt packages to install during the subiquity cloud-init installer.&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> [] </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;cloud_init_apt_mirror&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Sets the default apt mirror during the subiquity cloud-init installer.&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;post_install_scripts&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">list</span>(<span style="color:#66d9ef">string</span>) </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;A list of scripts and their relative paths to transfer and run after OS install.&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> [] </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;pre_final_scripts&#34;</span> { </span></span><span style="display:flex;"><span> type <span style="color:#f92672">=</span> <span style="color:#66d9ef">list</span>(<span style="color:#66d9ef">string</span>) </span></span><span style="display:flex;"><span> description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;A list of scripts and their relative paths to transfer and run before finalization.&#34;</span> </span></span><span style="display:flex;"><span> default <span style="color:#f92672">=</span> [] </span></span><span style="display:flex;"><span>}<span style="color:#75715e"> # [tl! collapse:end] </span></span></span></code></pre></div><p>(Collapsed because I think you get the idea, but feel free to expand to view the whole thing.)</p> <h4 id="input-variable-assignments"> Input Variable Assignments <a class="hlink" href="#input-variable-assignments"><i class="fa-solid fa-link"></i></a> </h4><p>Now that I've told Packer about the variables I intend to use, I can then go about setting values for those variables. That's done in the <code>linux-server.auto.pkrvars.hcl</code> file. I've highlighted the most interesting bits:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true} </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e">/* # </span></span></span><span style="display:flex;"><span><span style="color:#75715e"> Ubuntu Server 22.04 LTS variables used by the Packer Builder for Proxmox. </span></span></span><span style="display:flex;"><span><span style="color:#75715e">*/</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Guest</span> <span style="color:#66d9ef">Operating</span> <span style="color:#66d9ef">System</span> <span style="color:#66d9ef">Metadata</span> </span></span><span style="display:flex;"><span>vm_guest_os_keyboard <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;us&#34;</span> </span></span><span style="display:flex;"><span>vm_guest_os_language <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;en_US&#34;</span> </span></span><span style="display:flex;"><span>vm_guest_os_timezone <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;America/Chicago&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Virtual</span> <span style="color:#66d9ef">Machine</span> <span style="color:#66d9ef">Guest</span> <span style="color:#66d9ef">Operating</span> <span style="color:#66d9ef">System</span> <span style="color:#66d9ef">Setting</span> </span></span><span style="display:flex;"><span>vm_guest_os_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;l26&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span><span style="color:#66d9ef">Virtual</span> <span style="color:#66d9ef">Machine</span> <span style="color:#66d9ef">Guest</span> <span style="color:#66d9ef">Partition</span> <span style="color:#66d9ef">Sizes</span> (<span style="color:#66d9ef">in</span> <span style="color:#66d9ef">MB</span>) </span></span><span style="display:flex;"><span>vm_guest_part_audit <span style="color:#f92672">=</span> <span style="color:#ae81ff">4096</span><span style="color:#75715e"> # [tl! ~~:9] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>vm_guest_part_boot <span style="color:#f92672">=</span> <span style="color:#ae81ff">512</span> </span></span><span style="display:flex;"><span>vm_guest_part_efi <span style="color:#f92672">=</span> <span style="color:#ae81ff">512</span> </span></span><span style="display:flex;"><span>vm_guest_part_home <span style="color:#f92672">=</span> <span style="color:#ae81ff">8192</span> </span></span><span style="display:flex;"><span>vm_guest_part_log <span style="color:#f92672">=</span> <span style="color:#ae81ff">4096</span> </span></span><span style="display:flex;"><span>vm_guest_part_root <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span> </span></span><span style="display:flex;"><span>vm_guest_part_swap <span style="color:#f92672">=</span> <span style="color:#ae81ff">1024</span> </span></span><span style="display:flex;"><span>vm_guest_part_tmp <span style="color:#f92672">=</span> <span style="color:#ae81ff">4096</span> </span></span><span style="display:flex;"><span>vm_guest_part_var <span style="color:#f92672">=</span> <span style="color:#ae81ff">8192</span> </span></span><span style="display:flex;"><span>vm_guest_part_vartmp <span style="color:#f92672">=</span> <span style="color:#ae81ff">1024</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Virtual</span> <span style="color:#66d9ef">Machine</span> <span style="color:#66d9ef">Hardware</span> <span style="color:#66d9ef">Settings</span> </span></span><span style="display:flex;"><span>vm_cpu_cores <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span><span style="color:#75715e"> # [tl! ~~:8] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>vm_cpu_count <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span> </span></span><span style="display:flex;"><span>vm_cpu_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;host&#34;</span> </span></span><span style="display:flex;"><span>vm_disk_size <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;60G&#34;</span><span style="color:#75715e"> # </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>vm_bios_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ovmf&#34;</span> </span></span><span style="display:flex;"><span>vm_mem_size <span style="color:#f92672">=</span> <span style="color:#ae81ff">2048</span><span style="color:#75715e"> # </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>vm_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Ubuntu2204&#34;</span> </span></span><span style="display:flex;"><span>vm_network_card <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio&#34;</span> </span></span><span style="display:flex;"><span>vm_scsi_controller <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio-scsi-single&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Removable</span> <span style="color:#66d9ef">Media</span> <span style="color:#66d9ef">Settings</span> </span></span><span style="display:flex;"><span>iso_checksum_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;sha256&#34;</span><span style="color:#75715e"> # [tl! ~~:3] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>iso_checksum_value <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;45f873de9f8cb637345d6e66a583762730bbea30277ef7b32c9c3bd6700a32b2&#34;</span><span style="color:#75715e"> # </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>iso_file <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ubuntu-22.04.4-live-server-amd64.iso&#34;</span> </span></span><span style="display:flex;"><span>iso_url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://releases.ubuntu.com/jammy/ubuntu-22.04.4-live-server-amd64.iso&#34;</span> </span></span><span style="display:flex;"><span>remove_cdrom <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Boot</span> <span style="color:#66d9ef">Settings</span> </span></span><span style="display:flex;"><span>boot_key_interval <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;250ms&#34;</span> </span></span><span style="display:flex;"><span>vm_boot_wait <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;4s&#34;</span> </span></span><span style="display:flex;"><span>vm_boot_command <span style="color:#f92672">=</span> [<span style="color:#75715e"> # [tl! ~~:8] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#e6db74">&#34;&lt;esc&gt;&lt;wait&gt;c&#34;</span>, </span></span><span style="display:flex;"><span> &#34;linux /casper/vmlinuz --- autoinstall ds<span style="color:#f92672">=</span><span style="color:#960050;background-color:#1e0010">\</span><span style="color:#e6db74">&#34;nocloud\&#34;&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;&lt;enter&gt;&lt;wait5s&gt;&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;initrd /casper/initrd&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;&lt;enter&gt;&lt;wait5s&gt;&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;boot&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;&lt;enter&gt;&#34;</span> </span></span><span style="display:flex;"><span> ] </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Communicator</span> <span style="color:#66d9ef">Settings</span> </span></span><span style="display:flex;"><span>communicator_port <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span> </span></span><span style="display:flex;"><span>communicator_timeout <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;25m&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Provisioner</span> <span style="color:#66d9ef">Settings</span> </span></span><span style="display:flex;"><span>cloud_init_apt_packages <span style="color:#f92672">=</span> [<span style="color:#75715e"> # [tl! ~~:7] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#e6db74">&#34;cloud-guest-utils&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;net-tools&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;perl&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;qemu-guest-agent&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;vim&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;wget&#34;</span> </span></span><span style="display:flex;"><span>] </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>post_install_scripts <span style="color:#f92672">=</span> [<span style="color:#75715e"> # [tl! ~~:9] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#e6db74">&#34;scripts/linux/wait-for-cloud-init.sh&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;scripts/linux/cleanup-subiquity.sh&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;scripts/linux/install-ca-certs.sh&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;scripts/linux/disable-multipathd.sh&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;scripts/linux/prune-motd.sh&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;scripts/linux/persist-cloud-init-net.sh&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;scripts/linux/configure-pam_mkhomedir.sh&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;scripts/linux/update-packages.sh&#34;</span> </span></span><span style="display:flex;"><span>] </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>pre_final_scripts <span style="color:#f92672">=</span> [<span style="color:#75715e"> # [tl! ~~:6] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#e6db74">&#34;scripts/linux/cleanup-cloud-init.sh&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;scripts/linux/cleanup-packages.sh&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;builds/linux/ubuntu/22-04-lts/hardening.sh&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;scripts/linux/zero-disk.sh&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;scripts/linux/generalize.sh&#34;</span> </span></span><span style="display:flex;"><span>] </span></span></code></pre></div><p>As you can see, this sets up a lot of the properties which aren't strictly environment-specific, like:</p> <ul> <li>partition sizes (ll. 14-23),</li> <li>virtual hardware settings (ll. 26-34),</li> <li>the hash and URL for the installer ISO (ll. 37-40),</li> <li>the command to be run at first boot to start the installer in unattended mode (ll. 47-53),</li> <li>a list of packages to install during the <code>cloud-init</code> install phase, primarily the sort that might be needed during later steps (ll. 62-67),</li> <li>a list of scripts to execute after <code>cloud-init</code> (ll. 71-78),</li> <li>and a list of scripts to run at the very end of the process (ll. 82-86).</li> </ul> <p>We'll look at the specifics of those scripts shortly, but first...</p> <h4 id="packer-build-file"> Packer Build File <a class="hlink" href="#packer-build-file"><i class="fa-solid fa-link"></i></a> </h4><p>Let's explore the Packer build file, <code>linux-server.pkr.hcl</code>, which is the set of instructions used by Packer for performing the deployment. It's what ties everything else together.</p> <p>This one is kind of complex, so we'll take it a block or two at a time.</p> <p>It starts by setting the required minimum version of Packer and identifying what plugins (and versions) will be used to perform the build. I'm using the <a href="https://github.com/hashicorp/packer-plugin-proxmox" rel="external">Packer plugin for Proxmox↗</a> for executing the build on Proxmox, and the <a href="https://github.com/ivoronin/packer-plugin-sshkey" rel="external">Packer SSH key plugin↗</a> to simplify handling of SSH keys (we'll see how in the next block).</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true} </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e">/* # </span></span></span><span style="display:flex;"><span><span style="color:#75715e"> Ubuntu Server 22.04 LTS template using the Packer Builder for Proxmox. </span></span></span><span style="display:flex;"><span><span style="color:#75715e">*/</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">BLOCK</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">packer</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">The</span> <span style="color:#66d9ef">Packer</span> <span style="color:#66d9ef">configuration</span>. </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">packer</span> { </span></span><span style="display:flex;"><span> required_version <span style="color:#f92672">=</span> &#34;&gt;<span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>.<span style="color:#ae81ff">9</span>.<span style="color:#ae81ff">4</span><span style="color:#960050;background-color:#1e0010">&#34;</span><span style="color:#75715e"> # [tl! ~~] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#66d9ef">required_plugins</span> { </span></span><span style="display:flex;"><span> proxmox <span style="color:#f92672">=</span> {<span style="color:#75715e"> # [tl! ~~:2] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> version <span style="color:#f92672">=</span> &#34;&gt;<span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>.<span style="color:#ae81ff">1</span>.<span style="color:#ae81ff">8</span><span style="color:#960050;background-color:#1e0010">&#34;</span> </span></span><span style="display:flex;"><span> source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;github.com/hashicorp/proxmox&#34;</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> ssh-key <span style="color:#f92672">=</span> {<span style="color:#75715e"> # [tl! ~~:2] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> version <span style="color:#f92672">=</span> &#34;<span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>.<span style="color:#ae81ff">0</span>.<span style="color:#ae81ff">3</span><span style="color:#960050;background-color:#1e0010">&#34;</span> </span></span><span style="display:flex;"><span> source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;github.com/ivoronin/sshkey&#34;</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>This bit creates the <code>sshkey</code> data resource which uses the SSH plugin to generate a new SSH keypair to be used during the build process:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true, &#34;lineNumbersStart&#34;:22} </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">BLOCK</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">locals</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Defines</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">local</span> <span style="color:#66d9ef">variables</span>. </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Dynamically</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">generated</span> <span style="color:#66d9ef">SSH</span> <span style="color:#66d9ef">key</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;sshkey&#34; &#34;install&#34;</span> {<span style="color:#75715e"> # [tl! ~~:2] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ed25519&#34;</span> </span></span><span style="display:flex;"><span> name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;packer_key&#34;</span> </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>This first set of <code>locals {}</code> blocks take advantage of the dynamic nature of local variables. They call the <a href="https://developer.hashicorp.com/packer/docs/templates/hcl_templates/functions/contextual/vault" rel="external"><code>vault</code> function↗</a> to retrieve secrets from Vault and hold them as local variables. It's broken into a section for &quot;standard&quot; variables, which just hold configuration information like URLs and usernames, and one for &quot;sensitive&quot; variables like passwords and API tokens. The sensitive ones get <code>sensitive = true</code> to make sure they won't be printed in the logs anywhere.</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true, &#34;lineNumbersStart&#34;:31} </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#960050;background-color:#1e0010">//////////////////</span> <span style="color:#66d9ef">Vault</span> <span style="color:#66d9ef">Locals</span> <span style="color:#960050;background-color:#1e0010">//////////////////</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">To</span> <span style="color:#66d9ef">retrieve</span> <span style="color:#66d9ef">secrets</span> <span style="color:#66d9ef">from</span> <span style="color:#66d9ef">Vault</span>, <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">following</span> <span style="color:#66d9ef">environment</span> <span style="color:#66d9ef">variables</span> <span style="color:#66d9ef">MUST</span> <span style="color:#66d9ef">be</span> <span style="color:#66d9ef">defined</span><span style="color:#960050;background-color:#1e0010">:</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">VAULT_ADDR</span> <span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">base</span> <span style="color:#66d9ef">URL</span> <span style="color:#66d9ef">of</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">Vault</span> <span style="color:#66d9ef">server</span> (<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">https</span><span style="color:#960050;background-color:#1e0010">://</span><span style="color:#66d9ef">vault</span>.<span style="color:#66d9ef">example</span>.<span style="color:#66d9ef">com</span><span style="color:#960050;background-color:#1e0010">/&#39;</span>) </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">VAULT_TOKEN</span> <span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">token</span> <span style="color:#66d9ef">ID</span> <span style="color:#66d9ef">with</span> <span style="color:#66d9ef">rights</span> <span style="color:#66d9ef">to</span> <span style="color:#66d9ef">read</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">list</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Syntax</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">vault</span>() <span style="color:#66d9ef">call</span><span style="color:#960050;background-color:#1e0010">:</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;SECRET_ENGINE/data/SECRET_NAME&#34;, &#34;KEY&#34;</span>) </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Standard</span> <span style="color:#66d9ef">configuration</span> <span style="color:#66d9ef">values</span><span style="color:#960050;background-color:#1e0010">:</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">locals</span> {<span style="color:#75715e"> # [tl! ~~:10] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> build_public_key <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;packer/data/linux&#34;, &#34;public_key&#34;</span>) <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">SSH</span> <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">key</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">admin</span> <span style="color:#66d9ef">account</span> </span></span><span style="display:flex;"><span> build_username <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;packer/data/linux&#34;, &#34;username&#34;</span>) <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Username</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">admin</span> <span style="color:#66d9ef">account</span> </span></span><span style="display:flex;"><span> proxmox_url <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;packer/data/proxmox&#34;, &#34;api_url&#34;</span>) <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Proxmox</span> <span style="color:#66d9ef">API</span> <span style="color:#66d9ef">URL</span> </span></span><span style="display:flex;"><span> proxmox_insecure_connection <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;packer/data/proxmox&#34;, &#34;insecure_connection&#34;</span>) <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Allow</span> <span style="color:#66d9ef">insecure</span> <span style="color:#66d9ef">connections</span> <span style="color:#66d9ef">to</span> <span style="color:#66d9ef">Proxmox</span> </span></span><span style="display:flex;"><span> proxmox_node <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;packer/data/proxmox&#34;, &#34;node&#34;</span>) <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Proxmox</span> <span style="color:#66d9ef">node</span> <span style="color:#66d9ef">to</span> <span style="color:#66d9ef">use</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">build</span> </span></span><span style="display:flex;"><span> proxmox_token_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;packer/data/proxmox&#34;, &#34;token_id&#34;</span>) <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Proxmox</span> <span style="color:#66d9ef">token</span> <span style="color:#66d9ef">ID</span> </span></span><span style="display:flex;"><span> proxmox_iso_path <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;packer/data/proxmox&#34;, &#34;iso_path&#34;</span>) <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Path</span> <span style="color:#66d9ef">to</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">ISO</span> <span style="color:#66d9ef">storage</span> </span></span><span style="display:flex;"><span> proxmox_vm_storage_pool <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;packer/data/proxmox&#34;, &#34;vm_storage_pool&#34;</span>) <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Proxmox</span> <span style="color:#66d9ef">storage</span> <span style="color:#66d9ef">pool</span> <span style="color:#66d9ef">to</span> <span style="color:#66d9ef">use</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">build</span> </span></span><span style="display:flex;"><span> proxmox_iso_storage_pool <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;packer/data/proxmox&#34;, &#34;iso_storage_pool&#34;</span>) <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Proxmox</span> <span style="color:#66d9ef">storage</span> <span style="color:#66d9ef">pool</span> <span style="color:#66d9ef">to</span> <span style="color:#66d9ef">use</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">ISO</span> </span></span><span style="display:flex;"><span> proxmox_network_bridge <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;packer/data/proxmox&#34;, &#34;network_bridge&#34;</span>) <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Proxmox</span> <span style="color:#66d9ef">network</span> <span style="color:#66d9ef">bridge</span> <span style="color:#66d9ef">to</span> <span style="color:#66d9ef">use</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">build</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Sensitive</span> <span style="color:#66d9ef">values</span><span style="color:#960050;background-color:#1e0010">:</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">local</span> <span style="color:#e6db74">&#34;bootloader_password&#34;</span>{<span style="color:#75715e"> # [tl! ~~10] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> expression <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;packer/data/linux&#34;, &#34;bootloader_password&#34;</span>) <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Password</span> <span style="color:#66d9ef">to</span> <span style="color:#66d9ef">set</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">bootloader</span> </span></span><span style="display:flex;"><span> sensitive <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span><span style="color:#66d9ef">local</span> <span style="color:#e6db74">&#34;build_password_hash&#34;</span> { </span></span><span style="display:flex;"><span> expression <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;packer/data/linux&#34;, &#34;password_hash&#34;</span>) <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Password</span> <span style="color:#66d9ef">hash</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">admin</span> <span style="color:#66d9ef">account</span> </span></span><span style="display:flex;"><span> sensitive <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span><span style="color:#66d9ef">local</span> <span style="color:#e6db74">&#34;proxmox_token_secret&#34;</span> { </span></span><span style="display:flex;"><span> expression <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>(<span style="color:#e6db74">&#34;packer/data/proxmox&#34;, &#34;token_secret&#34;</span>) <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Token</span> <span style="color:#66d9ef">secret</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">authenticating</span> <span style="color:#66d9ef">to</span> <span style="color:#66d9ef">Proxmox</span> </span></span><span style="display:flex;"><span> sensitive <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//////////////////</span> <span style="color:#66d9ef">End</span> <span style="color:#66d9ef">Vault</span> <span style="color:#66d9ef">Locals</span> <span style="color:#960050;background-color:#1e0010">//////////////////</span> </span></span></code></pre></div><p>And the next <code>locals {}</code> block leverages other expressions to:</p> <ul> <li>dynamically set <code>local.build_date</code> to the current time (l. 70),</li> <li>combine individual string variables, like <code>local.iso_checksum</code> and <code>local.iso_path</code> (ll. 73-74),</li> <li>capture the keypair generated by the SSH key plugin (ll. 75-76),</li> <li>and use the <a href="https://developer.hashicorp.com/packer/docs/templates/hcl_templates/functions/file/templatefile" rel="external"><code>templatefile()</code> function↗</a> to process the <code>cloud-init</code> config file and insert appropriate variables (ll. 77-100)</li> </ul> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true, &#34;lineNumbersStart&#34;:69} </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">locals</span> { </span></span><span style="display:flex;"><span> build_date <span style="color:#f92672">=</span> <span style="color:#66d9ef">formatdate</span>(<span style="color:#e6db74">&#34;YYYY-MM-DD hh:mm ZZZ&#34;</span>, <span style="color:#66d9ef">timestamp</span>())<span style="color:#75715e"> # [tl! ~~] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> build_description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Ubuntu Server 22.04 LTS template\nBuild date: ${local.build_date}\nBuild tool: ${local.build_tool}&#34;</span> </span></span><span style="display:flex;"><span> build_tool <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;HashiCorp Packer ${packer.version}&#34;</span> </span></span><span style="display:flex;"><span> iso_checksum <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${var.iso_checksum_type}:${var.iso_checksum_value}&#34;</span><span style="color:#75715e"> # [tl! ~~:1] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> iso_path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${local.proxmox_iso_path}/${var.iso_file}&#34;</span> </span></span><span style="display:flex;"><span> ssh_private_key_file <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">sshkey</span>.<span style="color:#66d9ef">install</span>.<span style="color:#66d9ef">private_key_path</span><span style="color:#75715e"> # [tl! ~~:1] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> ssh_public_key <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">sshkey</span>.<span style="color:#66d9ef">install</span>.<span style="color:#66d9ef">public_key</span> </span></span><span style="display:flex;"><span> data_source_content <span style="color:#f92672">=</span> {<span style="color:#75715e"> # [tl! ~~:23] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> &#34;/meta-data&#34; <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;${abspath(path.root)}/data/meta-data&#34;</span>) </span></span><span style="display:flex;"><span> &#34;/user-data&#34; <span style="color:#f92672">=</span> <span style="color:#66d9ef">templatefile</span>(<span style="color:#e6db74">&#34;${abspath(path.root)}/data/user-data.pkrtpl.hcl&#34;</span>, { </span></span><span style="display:flex;"><span> apt_mirror <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">cloud_init_apt_mirror</span> </span></span><span style="display:flex;"><span> apt_packages <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">cloud_init_apt_packages</span> </span></span><span style="display:flex;"><span> build_password_hash <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">build_password_hash</span> </span></span><span style="display:flex;"><span> build_username <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">build_username</span> </span></span><span style="display:flex;"><span> ssh_keys <span style="color:#f92672">=</span> <span style="color:#66d9ef">concat</span>([<span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">ssh_public_key</span>], [<span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">build_public_key</span>]) </span></span><span style="display:flex;"><span> vm_guest_os_hostname <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_name</span> </span></span><span style="display:flex;"><span> vm_guest_os_keyboard <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_os_keyboard</span> </span></span><span style="display:flex;"><span> vm_guest_os_language <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_os_language</span> </span></span><span style="display:flex;"><span> vm_guest_os_timezone <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_os_timezone</span> </span></span><span style="display:flex;"><span> vm_guest_part_audit <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_part_audit</span> </span></span><span style="display:flex;"><span> vm_guest_part_boot <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_part_boot</span> </span></span><span style="display:flex;"><span> vm_guest_part_efi <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_part_efi</span> </span></span><span style="display:flex;"><span> vm_guest_part_home <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_part_home</span> </span></span><span style="display:flex;"><span> vm_guest_part_log <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_part_log</span> </span></span><span style="display:flex;"><span> vm_guest_part_root <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_part_root</span> </span></span><span style="display:flex;"><span> vm_guest_part_swap <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_part_swap</span> </span></span><span style="display:flex;"><span> vm_guest_part_tmp <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_part_tmp</span> </span></span><span style="display:flex;"><span> vm_guest_part_var <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_part_var</span> </span></span><span style="display:flex;"><span> vm_guest_part_vartmp <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_part_vartmp</span> </span></span><span style="display:flex;"><span> }) </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>The <code>source {}</code> block is where we get to the meat of the operation; it handles the actual creation of the virtual machine. This matches the input and local variables to the Packer options that tell it:</p> <ul> <li>how to connect and authenticate to the Proxmox host (ll. 110-113, 116),</li> <li>what virtual hardware settings the VM should have (ll. 119-141),</li> <li>that <code>local.data_source_content</code> (which contains the rendered <code>cloud-init</code> configuration - we'll look at that in a moment) should be mounted as a virtual CD-ROM device (ll. 144-149),</li> <li>to download and verify the installer ISO from <code>var.iso_url</code>, save it to <code>local.proxmox_iso_storage_pool</code>, and mount it as the primary CD-ROM device (ll. 150-155),</li> <li>what command to run at boot to start the install process (l. 159),</li> <li>and how to communicate with the VM once the install is underway (ll. 163-168).</li> </ul> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true, &#34;lineNumbersStart&#34;:104} </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">BLOCK</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">source</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Defines</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">builder</span> <span style="color:#66d9ef">configuration</span> <span style="color:#66d9ef">blocks</span>. </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">source</span> <span style="color:#e6db74">&#34;proxmox-iso&#34; &#34;linux-server&#34;</span> { </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Proxmox</span> <span style="color:#66d9ef">Endpoint</span> <span style="color:#66d9ef">Settings</span> <span style="color:#66d9ef">and</span> <span style="color:#66d9ef">Credentials</span> </span></span><span style="display:flex;"><span> insecure_skip_tls_verify <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">proxmox_insecure_connection</span><span style="color:#75715e"> # [tl! ~~:3] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> proxmox_url <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">proxmox_url</span> </span></span><span style="display:flex;"><span> token <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">proxmox_token_secret</span> </span></span><span style="display:flex;"><span> username <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">proxmox_token_id</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Node</span> <span style="color:#66d9ef">Settings</span> </span></span><span style="display:flex;"><span> node <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">proxmox_node</span><span style="color:#75715e"> # [tl! ~~] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> </span></span><span style="display:flex;"><span> <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Virtual</span> <span style="color:#66d9ef">Machine</span> <span style="color:#66d9ef">Settings</span> </span></span><span style="display:flex;"><span> bios <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ovmf&#34;</span><span style="color:#75715e"> # [tl! ~~:start] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> cores <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_cpu_cores</span> </span></span><span style="display:flex;"><span> cpu_type <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_cpu_type</span> </span></span><span style="display:flex;"><span> memory <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_mem_size</span> </span></span><span style="display:flex;"><span> os <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_guest_os_type</span> </span></span><span style="display:flex;"><span> scsi_controller <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_scsi_controller</span> </span></span><span style="display:flex;"><span> sockets <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_cpu_count</span> </span></span><span style="display:flex;"><span> template_description <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">build_description</span> </span></span><span style="display:flex;"><span> template_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_name</span> </span></span><span style="display:flex;"><span> vm_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_name</span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">disks</span> { </span></span><span style="display:flex;"><span> disk_size <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_disk_size</span> </span></span><span style="display:flex;"><span> storage_pool <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">proxmox_vm_storage_pool</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">efi_config</span> { </span></span><span style="display:flex;"><span> efi_storage_pool <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">proxmox_vm_storage_pool</span> </span></span><span style="display:flex;"><span> efi_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;4m&#34;</span> </span></span><span style="display:flex;"><span> pre_enrolled_keys <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">network_adapters</span> { </span></span><span style="display:flex;"><span> bridge <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">proxmox_network_bridge</span> </span></span><span style="display:flex;"><span> model <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_network_model</span> </span></span><span style="display:flex;"><span> }<span style="color:#75715e"> # [tl! ~~:end] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> </span></span><span style="display:flex;"><span> <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Removable</span> <span style="color:#66d9ef">Media</span> <span style="color:#66d9ef">Settings</span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">additional_iso_files</span> {<span style="color:#75715e"> # [tl! ~~:5] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> cd_content <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">data_source_content</span> </span></span><span style="display:flex;"><span> cd_label <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">cd_label</span> </span></span><span style="display:flex;"><span> iso_storage_pool <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">proxmox_iso_storage_pool</span> </span></span><span style="display:flex;"><span> unmount <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">remove_cdrom</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> iso_checksum <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">iso_checksum</span><span style="color:#75715e"> # [tl! ~~:5] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> // iso_file <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">iso_path</span> </span></span><span style="display:flex;"><span> iso_url <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">iso_url</span> </span></span><span style="display:flex;"><span> iso_download_pve <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> iso_storage_pool <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">proxmox_iso_storage_pool</span> </span></span><span style="display:flex;"><span> unmount_iso <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">remove_cdrom</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Boot</span> <span style="color:#66d9ef">and</span> <span style="color:#66d9ef">Provisioning</span> <span style="color:#66d9ef">Settings</span> </span></span><span style="display:flex;"><span> boot_command <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_boot_command</span><span style="color:#75715e"> # [tl! ~~] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> boot_wait <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vm_boot_wait</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Communicator</span> <span style="color:#66d9ef">Settings</span> <span style="color:#66d9ef">and</span> <span style="color:#66d9ef">Credentials</span> </span></span><span style="display:flex;"><span> communicator <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ssh&#34;</span><span style="color:#75715e"> # [tl! ~~:5] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> ssh_clear_authorized_keys <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">build_remove_keys</span> </span></span><span style="display:flex;"><span> ssh_port <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">communicator_port</span> </span></span><span style="display:flex;"><span> ssh_private_key_file <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">ssh_private_key_file</span> </span></span><span style="display:flex;"><span> ssh_timeout <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">communicator_timeout</span> </span></span><span style="display:flex;"><span> ssh_username <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">build_username</span> </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>By this point, we've got a functional virtual machine running on the Proxmox host but there are still some additional tasks to perform before it can be converted to a template. That's where the <code>build {}</code> block comes in: it connects to the VM and runs a few <code>provisioner</code> steps:</p> <ul> <li>The <code>file</code> provisioner is used to copy any certificate files into the VM at <code>/tmp</code> (ll. 181-182) and to copy the <a href="https://github.com/jbowdre/packer-proxmox-templates/blob/main/scripts/linux/join-domain.sh" rel="external"><code>join-domain.sh</code> script↗</a> into the initial user's home directory (ll. 186-187).</li> <li>The first <code>shell</code> provisioner loops through and executes all the scripts listed in <code>var.post_install_scripts</code> (ll. 191-193). The last script in that list (<code>update-packages.sh</code>) finishes with a reboot for good measure.</li> <li>The second <code>shell</code> provisioner (ll. 197-203) waits for 30 seconds for the reboot to complete before it picks up with the remainder of the scripts, and it passes in the bootloader password for use by the hardening script.</li> </ul> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true, &#34;lineNumbersStart&#34;:171} </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">BLOCK</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">build</span> </span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">//</span> <span style="color:#66d9ef">Defines</span> <span style="color:#66d9ef">the</span> <span style="color:#66d9ef">builders</span> <span style="color:#66d9ef">to</span> <span style="color:#66d9ef">run</span>, <span style="color:#66d9ef">provisioners</span>, <span style="color:#66d9ef">and</span> <span style="color:#66d9ef">post</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">processors</span>. </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">build</span> { </span></span><span style="display:flex;"><span> sources <span style="color:#f92672">=</span> [ </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;source.proxmox-iso.linux-server&#34;</span> </span></span><span style="display:flex;"><span> ] </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">provisioner</span> <span style="color:#e6db74">&#34;file&#34;</span> { </span></span><span style="display:flex;"><span> source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;certs&#34;</span><span style="color:#75715e"> # [tl! ~~:1] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> destination <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/tmp&#34;</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">provisioner</span> <span style="color:#e6db74">&#34;file&#34;</span> { </span></span><span style="display:flex;"><span> source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;scripts/linux/join-domain.sh&#34;</span><span style="color:#75715e"> # [tl! ~~:1] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> destination <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/home/${local.build_username}/join-domain.sh&#34;</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">provisioner</span> <span style="color:#e6db74">&#34;shell&#34;</span> { </span></span><span style="display:flex;"><span> execute_command <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;bash {{ .Path }}&#34;</span><span style="color:#75715e"> # [tl! ~~:2] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> expect_disconnect <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> scripts <span style="color:#f92672">=</span> <span style="color:#66d9ef">formatlist</span>(<span style="color:#e6db74">&#34;${path.cwd}/%s&#34;</span>, <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">post_install_scripts</span>) </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">provisioner</span> <span style="color:#e6db74">&#34;shell&#34;</span> { </span></span><span style="display:flex;"><span> env <span style="color:#f92672">=</span> {<span style="color:#75715e"> # [tl! ~~:6] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> &#34;BOOTLOADER_PASSWORD&#34; <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">bootloader_password</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> execute_command <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;{{ .Vars }} bash {{ .Path }}&#34;</span> </span></span><span style="display:flex;"><span> expect_disconnect <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> pause_before <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;30s&#34;</span> </span></span><span style="display:flex;"><span> scripts <span style="color:#f92672">=</span> <span style="color:#66d9ef">formatlist</span>(<span style="color:#e6db74">&#34;${path.cwd}/%s&#34;</span>, <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">pre_final_scripts</span>) </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><h4 id="cloud-init-config"> <code>cloud-init</code> Config <a class="hlink" href="#cloud-init-config"><i class="fa-solid fa-link"></i></a> </h4><p>Now let's back up a bit and drill into that <code>cloud-init</code> template file, <code>builds/linux/ubuntu/22-04-lts/data/user-data.pkrtpl.hcl</code>, which is loaded during the <code>source {}</code> block to tell the OS installer how to configure things on the initial boot.</p> <p>The file follows the basic YAML-based syntax of a standard <a href="https://cloudinit.readthedocs.io/en/latest/reference/examples.html" rel="external">cloud config file↗</a>, but with some <a href="https://developer.hashicorp.com/packer/docs/templates/hcl_templates/functions/file/templatefile" rel="external">HCL templating↗</a> to pull in certain values from elsewhere.</p> <p>Some of the key tasks handled by this configuration include:</p> <ul> <li>stopping the SSH server (l. 10),</li> <li>setting the hostname (l. 12), inserting username and password (ll. 13-14),</li> <li>enabling (temporary) passwordless-sudo (ll. 17-18),</li> <li>installing a templated list of packages (ll. 30-35),</li> <li>inserting a templated list of SSH public keys (ll. 39-44),</li> <li>installing all package updates, disabling root logins, and setting the timezone (ll. 206-208)</li> <li>and other needful things like setting up drive partitioning.</li> </ul> <p><code>cloud-init</code> will reboot the VM once it completes, and when it comes back online it will have a DHCP-issued IP address and the accounts/credentials needed for Packer to log in via SSH and continue the setup in the <code>build {}</code> block.</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#cloud-config</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">autoinstall</span>: </span></span><span style="display:flex;"><span><span style="color:#ae81ff">%{ if length( apt_mirror ) &gt; 0 ~}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">apt</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">primary</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f92672">arches</span>: [<span style="color:#ae81ff">default]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uri</span>: <span style="color:#e6db74">&#34;${ apt_mirror }&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#ae81ff">%{ endif ~}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">early-commands</span>: <span style="color:#75715e"># [tl! **:5]</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">sudo systemctl stop ssh</span> <span style="color:#75715e"># [tl! ~~]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">identity</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">hostname</span>: <span style="color:#ae81ff">${ vm_guest_os_hostname }</span> <span style="color:#75715e"># [tl! ~~:2]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">password</span>: <span style="color:#e6db74">&#39;${ build_password_hash }&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">username</span>: <span style="color:#ae81ff">${ build_username }</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">keyboard</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">layout</span>: <span style="color:#ae81ff">${ vm_guest_os_keyboard }</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">late-commands</span>: <span style="color:#75715e"># [tl! **:2]</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">echo &#34;${ build_username } ALL=(ALL) NOPASSWD:ALL&#34; &gt; /target/etc/sudoers.d/${ build_username }</span> <span style="color:#75715e"># [tl! ~~:1]</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">curtin in-target --target=/target -- chmod 400 /etc/sudoers.d/${ build_username }</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">locale</span>: <span style="color:#ae81ff">${ vm_guest_os_language }</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">network</span>: <span style="color:#75715e"># [tl! collapse:9]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">network</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">version</span>: <span style="color:#ae81ff">2</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">ethernets</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">mainif</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">match</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">name</span>: <span style="color:#ae81ff">e*</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">critical</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">dhcp4</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">dhcp-identifier</span>: <span style="color:#ae81ff">mac</span> </span></span><span style="display:flex;"><span><span style="color:#ae81ff">%{ if length( apt_packages ) &gt; 0 ~}</span> <span style="color:#75715e"># [tl! **:5]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">packages</span>: </span></span><span style="display:flex;"><span><span style="color:#ae81ff">%{ for package in apt_packages ~}</span> <span style="color:#75715e"># [tl! ~~:2]</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">${ package }</span> </span></span><span style="display:flex;"><span><span style="color:#ae81ff">%{ endfor ~}</span> </span></span><span style="display:flex;"><span><span style="color:#ae81ff">%{ endif ~}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">ssh</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">install-server</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">allow-pw</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span><span style="color:#ae81ff">%{ if length( ssh_keys ) &gt; 0 ~}</span> <span style="color:#75715e"># [tl! **:5]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">authorized-keys</span>: </span></span><span style="display:flex;"><span><span style="color:#ae81ff">%{ for ssh_key in ssh_keys ~}</span> <span style="color:#75715e"># [tl! ~~2]</span> </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">${ ssh_key }</span> </span></span><span style="display:flex;"><span><span style="color:#ae81ff">%{ endfor ~}</span> </span></span><span style="display:flex;"><span><span style="color:#ae81ff">%{ endif ~}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">storage</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">config</span>: <span style="color:#75715e"># [tl! collapse:start]</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">ptable</span>: <span style="color:#ae81ff">gpt</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/dev/sda</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">wipe</span>: <span style="color:#ae81ff">superblock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">disk</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">disk-sda</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">device</span>: <span style="color:#ae81ff">disk-sda</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">size</span>: <span style="color:#ae81ff">${ vm_guest_part_efi }M</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">wipe</span>: <span style="color:#ae81ff">superblock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">flag</span>: <span style="color:#ae81ff">boot</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">number</span>: <span style="color:#ae81ff">1</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">grub_device</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">partition</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">partition-0</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">fstype</span>: <span style="color:#ae81ff">fat32</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volume</span>: <span style="color:#ae81ff">partition-0</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">label</span>: <span style="color:#ae81ff">EFIFS</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">format</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">format-efi</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">device</span>: <span style="color:#ae81ff">disk-sda</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">size</span>: <span style="color:#ae81ff">${ vm_guest_part_boot }M</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">wipe</span>: <span style="color:#ae81ff">superblock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">number</span>: <span style="color:#ae81ff">2</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">partition</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">partition-1</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">fstype</span>: <span style="color:#ae81ff">xfs</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volume</span>: <span style="color:#ae81ff">partition-1</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">label</span>: <span style="color:#ae81ff">BOOTFS</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">format</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">format-boot</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">device</span>: <span style="color:#ae81ff">disk-sda</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">size</span>: -<span style="color:#ae81ff">1</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">wipe</span>: <span style="color:#ae81ff">superblock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">number</span>: <span style="color:#ae81ff">3</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">partition</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">partition-2</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">sysvg</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">devices</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">partition-2</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">lvm_volgroup</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">lvm_volgroup-0</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">home</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volgroup</span>: <span style="color:#ae81ff">lvm_volgroup-0</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">size</span>: <span style="color:#ae81ff">${ vm_guest_part_home}M</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">wipe</span>: <span style="color:#ae81ff">superblock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">lvm_partition</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">lvm_partition-home</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">fstype</span>: <span style="color:#ae81ff">xfs</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volume</span>: <span style="color:#ae81ff">lvm_partition-home</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">format</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">label</span>: <span style="color:#ae81ff">HOMEFS</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">format-home</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">tmp</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volgroup</span>: <span style="color:#ae81ff">lvm_volgroup-0</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">size</span>: <span style="color:#ae81ff">${ vm_guest_part_tmp }M</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">wipe</span>: <span style="color:#ae81ff">superblock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">lvm_partition</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">lvm_partition-tmp</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">fstype</span>: <span style="color:#ae81ff">xfs</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volume</span>: <span style="color:#ae81ff">lvm_partition-tmp</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">format</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">label</span>: <span style="color:#ae81ff">TMPFS</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">format-tmp</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">var</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volgroup</span>: <span style="color:#ae81ff">lvm_volgroup-0</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">size</span>: <span style="color:#ae81ff">${ vm_guest_part_var }M</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">wipe</span>: <span style="color:#ae81ff">superblock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">lvm_partition</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">lvm_partition-var</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">fstype</span>: <span style="color:#ae81ff">xfs</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volume</span>: <span style="color:#ae81ff">lvm_partition-var</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">format</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">label</span>: <span style="color:#ae81ff">VARFS</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">format-var</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">log</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volgroup</span>: <span style="color:#ae81ff">lvm_volgroup-0</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">size</span>: <span style="color:#ae81ff">${ vm_guest_part_log }M</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">wipe</span>: <span style="color:#ae81ff">superblock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">lvm_partition</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">lvm_partition-log</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">fstype</span>: <span style="color:#ae81ff">xfs</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volume</span>: <span style="color:#ae81ff">lvm_partition-log</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">format</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">label</span>: <span style="color:#ae81ff">LOGFS</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">format-log</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">audit</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volgroup</span>: <span style="color:#ae81ff">lvm_volgroup-0</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">size</span>: <span style="color:#ae81ff">${ vm_guest_part_audit }M</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">wipe</span>: <span style="color:#ae81ff">superblock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">lvm_partition</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">lvm_partition-audit</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">fstype</span>: <span style="color:#ae81ff">xfs</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volume</span>: <span style="color:#ae81ff">lvm_partition-audit</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">format</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">label</span>: <span style="color:#ae81ff">AUDITFS</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">format-audit</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">vartmp</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volgroup</span>: <span style="color:#ae81ff">lvm_volgroup-0</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">size</span>: <span style="color:#ae81ff">${ vm_guest_part_vartmp }M</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">wipe</span>: <span style="color:#ae81ff">superblock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">lvm_partition</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">lvm_partition-vartmp</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">fstype</span>: <span style="color:#ae81ff">xfs</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volume</span>: <span style="color:#ae81ff">lvm_partition-vartmp</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">format</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">label</span>: <span style="color:#ae81ff">VARTMPFS</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">format-vartmp</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">root</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volgroup</span>: <span style="color:#ae81ff">lvm_volgroup-0</span> </span></span><span style="display:flex;"><span><span style="color:#ae81ff">%{ if vm_guest_part_root == 0 ~}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">size</span>: -<span style="color:#ae81ff">1</span> </span></span><span style="display:flex;"><span><span style="color:#ae81ff">%{ else ~}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">size</span>: <span style="color:#ae81ff">${ vm_guest_part_root }M</span> </span></span><span style="display:flex;"><span><span style="color:#ae81ff">%{ endif ~}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">wipe</span>: <span style="color:#ae81ff">superblock</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">lvm_partition</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">lvm_partition-root</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">fstype</span>: <span style="color:#ae81ff">xfs</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">volume</span>: <span style="color:#ae81ff">lvm_partition-root</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">format</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">label</span>: <span style="color:#ae81ff">ROOTFS</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">format-root</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">device</span>: <span style="color:#ae81ff">format-root</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">mount</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">mount-root</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/boot</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">device</span>: <span style="color:#ae81ff">format-boot</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">mount</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">mount-boot</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/boot/efi</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">device</span>: <span style="color:#ae81ff">format-efi</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">mount</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">mount-efi</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/home</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">device</span>: <span style="color:#ae81ff">format-home</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">mount</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">mount-home</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/tmp</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">device</span>: <span style="color:#ae81ff">format-tmp</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">mount</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">mount-tmp</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/var</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">device</span>: <span style="color:#ae81ff">format-var</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">mount</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">mount-var</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/var/log</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">device</span>: <span style="color:#ae81ff">format-log</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">mount</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">mount-log</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/var/log/audit</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">device</span>: <span style="color:#ae81ff">format-audit</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">mount</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">mount-audit</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/var/tmp</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">device</span>: <span style="color:#ae81ff">format-vartmp</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">mount</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">id</span>: <span style="color:#ae81ff">mount-vartmp</span> <span style="color:#75715e"># [tl! collapse:end]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">user-data</span>: <span style="color:#75715e"># [tl! **:3]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">package_upgrade</span>: <span style="color:#66d9ef">true</span> <span style="color:#75715e"># [tl! ~~:2]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">disable_root</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">timezone</span>: <span style="color:#ae81ff">${ vm_guest_os_timezone }</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">version</span>: <span style="color:#ae81ff">1</span> </span></span></code></pre></div><h4 id="setup-scripts"> Setup Scripts <a class="hlink" href="#setup-scripts"><i class="fa-solid fa-link"></i></a> </h4><p>After the <code>cloud-init</code> setup is completed, Packer control gets passed to the <code>build {}</code> block and the provisioners there run through a series of scripts to perform additional configuration of the guest OS. I split the scripts into two sets, which I called <code>post_install_scripts</code> and <code>pre_final_scripts</code>, with a reboot that happens in between them.</p> <h5 id="post-install"> Post Install <a class="hlink" href="#post-install"><i class="fa-solid fa-link"></i></a> </h5><p>The post-install scripts run after the <code>cloud-init</code> installation has completed, and (depending on the exact Linux flavor) may include:</p> <ol> <li><code>wait-for-cloud-init.sh</code>, which just checks to confirm that <code>cloud-init</code> has truly finished before proceeding: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># waits for cloud-init to finish before proceeding</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Waiting for cloud-init...&#39;</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">while</span> <span style="color:#f92672">[</span> ! -f /var/lib/cloud/instance/boot-finished <span style="color:#f92672">]</span>; <span style="color:#66d9ef">do</span> </span></span><span style="display:flex;"><span> sleep <span style="color:#ae81ff">1</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span> </span></span></code></pre></div></li> <li><code>cleanup-subiquity.sh</code> to remove the default network configuration generated by the Ubuntu installer: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># cleans up cloud-init config from subiquity</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -f /etc/cloud/cloud.cfg.d/99-installer.cfg <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> sudo rm /etc/cloud/cloud.cfg.d/99-installer.cfg </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;&gt;&gt; Deleting subiquity cloud-init config...&#39;</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -f /etc/cloud/cloud.cfg.d/subiquity-disable-cloudinit-networking.cfg <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> sudo rm /etc/cloud/cloud.cfg.d/subiquity-disable-cloudinit-networking.cfg </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;&gt;&gt; Deleting subiquity cloud-init network config...&#39;</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span></code></pre></div></li> <li><code>install-ca-certs.sh</code> to install any trusted CA certs which were in the <code>certs/</code> folder of the Packer environment and copied to <code>/tmp/certs/</code> in the guest: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># installs trusted CA certs from /tmp/certs/</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> awk -F<span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;/^ID/{print $2}&#39;</span> /etc/os-release | grep -q debian; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;&gt;&gt; Installing certificates...&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> ls /tmp/certs/*.cer &gt;/dev/null 2&gt;&amp;1; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> sudo cp /tmp/certs/* /usr/local/share/ca-certificates/ </span></span><span style="display:flex;"><span> cd /usr/local/share/ca-certificates/ </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> file in *.cer; <span style="color:#66d9ef">do</span> </span></span><span style="display:flex;"><span> sudo mv -- <span style="color:#e6db74">&#34;</span>$file<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>file%.cer<span style="color:#e6db74">}</span><span style="color:#e6db74">.crt&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">done</span> </span></span><span style="display:flex;"><span> sudo /usr/sbin/update-ca-certificates </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">else</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;No certs to install.&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">fi</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">elif</span> awk -F<span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;/^ID/{print $2}&#39;</span> /etc/os-release | grep -q rhel; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;&gt;&gt; Installing certificates...&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> ls /tmp/certs/*.cer &gt;/dev/null 2&gt;&amp;1; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> sudo cp /tmp/certs/* /etc/pki/ca-trust/source/anchors/ </span></span><span style="display:flex;"><span> cd /etc/pki/ca-trust/source/anchors/ </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> file in *.cer; <span style="color:#66d9ef">do</span> </span></span><span style="display:flex;"><span> sudo mv -- <span style="color:#e6db74">&#34;</span>$file<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>file%.cer<span style="color:#e6db74">}</span><span style="color:#e6db74">.crt&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">done</span> </span></span><span style="display:flex;"><span> sudo /bin/update-ca-trust </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">else</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;No certs to install.&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">fi</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span></code></pre></div></li> <li><code>disable-multipathd.sh</code> to, uh, <em>disable multipathd</em> to keep things lightweight and simple: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># disables multipathd</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Disabling multipathd...&#39;</span> </span></span><span style="display:flex;"><span>sudo systemctl disable multipathd </span></span></code></pre></div></li> <li><code>prune-motd.sh</code> to remove those noisy, promotional default messages that tell you to enable cockpit or check out Ubuntu Pro or whatever: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># prunes default noisy MOTD</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Pruning default MOTD...&#39;</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> awk -F<span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;/^ID/{print $2}&#39;</span> /etc/os-release | grep -q rhel; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -L <span style="color:#e6db74">&#34;/etc/motd.d/insights-client&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> sudo unlink /etc/motd.d/insights-client </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">fi</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">elif</span> awk -F<span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;/^ID/{print $2}&#39;</span> /etc/os-release | grep -q debian; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> sudo chmod -x /etc/update-motd.d/91-release-upgrade </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span></code></pre></div></li> <li><code>persist-cloud-init-net.sh</code> to ensure the <code>cloud-init</code> cache isn't wiped on reboot so the network settings will stick: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># ensures network settings are preserved on boot</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Preserving network settings...&#39;</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> grep -q <span style="color:#e6db74">&#39;manual_cache_clean&#39;</span> /etc/cloud/cloud.cfg; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> sudo sed -i <span style="color:#e6db74">&#39;s/^manual_cache_clean.*$/manual_cache_clean: True/&#39;</span> /etc/cloud/cloud.cfg </span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;manual_cache_clean: True&#39;</span> | sudo tee -a /etc/cloud/cloud.cfg </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span></code></pre></div></li> <li><code>configure-pam_mkhomedir.sh</code> to configure the <code>pam_mkhomedir</code> module to create user homedirs with the appropriate (<code>750</code>) permission set: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># configures pam_mkhomedir to create home directories with 750 permissions</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Configuring pam_mkhomedir...&#39;</span> </span></span><span style="display:flex;"><span>sudo sed -i <span style="color:#e6db74">&#39;s/optional.*pam_mkhomedir.so/required\t\tpam_mkhomedir.so umask=0027/&#39;</span> /usr/share/pam-configs/mkhomedir </span></span></code></pre></div></li> <li><code>update-packages.sh</code> to install any available package updates and reboot: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># updates packages and reboots</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> awk -F<span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;/^ID/{print $2}&#39;</span> /etc/os-release | grep -q rhel; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> which dnf &amp;&gt;/dev/null; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;&gt;&gt; Checking for and installing updates...&#39;</span> </span></span><span style="display:flex;"><span> sudo dnf -y update </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">else</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;&gt;&gt; Checking for and installing updates...&#39;</span> </span></span><span style="display:flex;"><span> sudo yum -y update </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">fi</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;&gt;&gt; Rebooting!&#39;</span> </span></span><span style="display:flex;"><span> sudo reboot </span></span><span style="display:flex;"><span><span style="color:#66d9ef">elif</span> awk -F<span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;/^ID/{print $2}&#39;</span> /etc/os-release | grep -q debian; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;&gt;&gt; Checking for and installing updates...&#39;</span> </span></span><span style="display:flex;"><span> sudo apt-get update <span style="color:#f92672">&amp;&amp;</span> sudo apt-get -y upgrade </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;&gt;&gt; Rebooting!&#39;</span> </span></span><span style="display:flex;"><span> sudo reboot </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span></code></pre></div></li> </ol> <p>After the reboot, the process picks back up with the pre-final scripts.</p> <h5 id="pre-final"> Pre-Final <a class="hlink" href="#pre-final"><i class="fa-solid fa-link"></i></a> </h5><ol> <li><code>cleanup-cloud-init.sh</code> performs a <a href="https://cloudinit.readthedocs.io/en/latest/reference/cli.html#clean" rel="external"><code>clean</code>↗</a> action to get the template ready to be re-used: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># cleans up cloud-init state</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Cleaning up cloud-init state...&#39;</span> </span></span><span style="display:flex;"><span>sudo cloud-init clean -l </span></span></code></pre></div></li> <li><code>cleanup-packages.sh</code> uninstalls packages and kernel versions which are no longer needed: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># cleans up unneeded packages to reduce the size of the image</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> awk -F<span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;/^ID/{print $2}&#39;</span> /etc/os-release | grep -q debian; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;&gt;&gt; Cleaning up unneeded packages...&#39;</span> </span></span><span style="display:flex;"><span> sudo apt-get -y autoremove --purge </span></span><span style="display:flex;"><span> sudo apt-get -y clean </span></span><span style="display:flex;"><span><span style="color:#66d9ef">elif</span> awk -F<span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;/^ID/{print $2}&#39;</span> /etc/os-release | grep -q rhel; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> which dnf &amp;&gt;/dev/null; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;&gt;&gt; Cleaning up unneeded packages...&#39;</span> </span></span><span style="display:flex;"><span> sudo dnf -y remove linux-firmware </span></span><span style="display:flex;"><span> sudo dnf -y remove <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>dnf repoquery --installonly --latest-limit<span style="color:#f92672">=</span>-1 -q<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> </span></span><span style="display:flex;"><span> sudo dnf -y autoremove </span></span><span style="display:flex;"><span> sudo dnf -y clean all --enablerepo<span style="color:#f92672">=</span><span style="color:#ae81ff">\*</span>; </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">else</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;&gt;&gt; Cleaning up unneeded packages...&#39;</span> </span></span><span style="display:flex;"><span> sudo yum -y remove linux-firmware </span></span><span style="display:flex;"><span> sudo package-cleanup --oldkernels --count<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span> </span></span><span style="display:flex;"><span> sudo yum -y autoremove </span></span><span style="display:flex;"><span> sudo yum -y clean all --enablerepo<span style="color:#f92672">=</span><span style="color:#ae81ff">\*</span>; </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">fi</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span></code></pre></div></li> <li><code>build/linux/22-04-lts/hardening.sh</code> is a build-specific script to perform basic hardening tasks toward the CIS Level 2 server benchmark. It doesn't have a lot of fancy logic because it is <em>only intended to be run during this package process</em> when it's making modifications from a known state. It's long, so I won't repost it here, and I may end up writing a separate post specifically about this hardening process, but you're welcome to view the full script for <a href="https://github.com/jbowdre/packer-proxmox-templates/blob/main/builds/linux/ubuntu/22-04-lts/hardening.sh" rel="external">Ubuntu 22.04 here↗</a>.</li> <li><code>zero-disk.sh</code> fills a file with zeroes until the disk runs out of space, and then removes it, resulting in a reduced template image size: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># zeroes out free space to reduce disk size</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Zeroing free space to reduce disk size...&#39;</span> </span></span><span style="display:flex;"><span>sudo sh -c <span style="color:#e6db74">&#39;dd if=/dev/zero of=/EMPTY bs=1M || true; sync; sleep 1; sync&#39;</span> </span></span><span style="display:flex;"><span>sudo sh -c <span style="color:#e6db74">&#39;rm -f /EMPTY; sync; sleep 1; sync&#39;</span> </span></span></code></pre></div></li> <li><code>generalize.sh</code> performs final steps to get the template ready for cloning, including removing the <code>sudoers.d</code> configuration which allowed passwordless elevation during the setup: <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># prepare a VM to become a template.</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Clearing audit logs...&#39;</span> </span></span><span style="display:flex;"><span>sudo sh -c <span style="color:#e6db74">&#39;if [ -f /var/log/audit/audit.log ]; then </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> cat /dev/null &gt; /var/log/audit/audit.log </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> fi&#39;</span> </span></span><span style="display:flex;"><span>sudo sh -c <span style="color:#e6db74">&#39;if [ -f /var/log/wtmp ]; then </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> cat /dev/null &gt; /var/log/wtmp </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> fi&#39;</span> </span></span><span style="display:flex;"><span>sudo sh -c <span style="color:#e6db74">&#39;if [ -f /var/log/lastlog ]; then </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> cat /dev/null &gt; /var/log/lastlog </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> fi&#39;</span> </span></span><span style="display:flex;"><span>sudo sh -c <span style="color:#e6db74">&#39;if [ -f /etc/logrotate.conf ]; then </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> logrotate -f /etc/logrotate.conf 2&gt;/dev/null </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> fi&#39;</span> </span></span><span style="display:flex;"><span>sudo rm -rf /var/log/journal/* </span></span><span style="display:flex;"><span>sudo rm -f /var/lib/dhcp/* </span></span><span style="display:flex;"><span>sudo find /var/log -type f -delete </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Clearing persistent udev rules...&#39;</span> </span></span><span style="display:flex;"><span>sudo sh -c <span style="color:#e6db74">&#39;if [ -f /etc/udev/rules.d/70-persistent-net.rules ]; then </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> rm /etc/udev/rules.d/70-persistent-net.rules </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> fi&#39;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># check for only RHEL releases</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> awk -F<span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;/^ID=/{print $2}&#39;</span> /etc/os-release | grep -q rhel; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#39;&gt;&gt; Clearing RHSM subscription...&#39;</span> </span></span><span style="display:flex;"><span> sudo subscription-manager unregister </span></span><span style="display:flex;"><span> sudo subscription-manager clean </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Clearing temp dirs...&#39;</span> </span></span><span style="display:flex;"><span>sudo rm -rf /tmp/* </span></span><span style="display:flex;"><span>sudo rm -rf /var/tmp/* </span></span><span style="display:flex;"><span><span style="color:#75715e"># check for RHEL-like releases (RHEL and Rocky)</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> awk -F<span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;/^ID/{print $2}&#39;</span> /etc/os-release | grep -q rhel; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> sudo rm -rf /var/cache/dnf/* </span></span><span style="display:flex;"><span> sudo rm -rf /var/log/rhsm/* </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Clearing host keys...&#39;</span> </span></span><span style="display:flex;"><span>sudo rm -f /etc/ssh/ssh_host_* </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Removing Packer SSH key...&#39;</span> </span></span><span style="display:flex;"><span>sed -i <span style="color:#e6db74">&#39;/packer_key/d&#39;</span> ~/.ssh/authorized_keys </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Clearing machine-id...&#39;</span> </span></span><span style="display:flex;"><span>sudo truncate -s <span style="color:#ae81ff">0</span> /etc/machine-id </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -f /var/lib/dbus/machine-id <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> sudo rm -f /var/lib/dbus/machine-id </span></span><span style="display:flex;"><span> sudo ln -s /etc/machine-id /var/lib/dbus/machine-id </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Clearing shell history...&#39;</span> </span></span><span style="display:flex;"><span>unset HISTFILE </span></span><span style="display:flex;"><span>history -cw </span></span><span style="display:flex;"><span>echo &gt; ~/.bash_history </span></span><span style="display:flex;"><span>sudo rm -f /root/.bash_history </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;&gt;&gt; Clearing sudoers.d...&#39;</span> </span></span><span style="display:flex;"><span>sudo rm -f /etc/sudoers.d/* </span></span></code></pre></div></li> </ol> <h3 id="packer-build"> Packer Build <a class="hlink" href="#packer-build"><i class="fa-solid fa-link"></i></a> </h3><p>At this point, I should (in theory) be able to kick off the build from my laptop with a Packer command - but first, I'll need to set up some environment variables so that Packer will be able to communicate with my Vault server:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>export VAULT_ADDR<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://vault.tailnet-name.ts.net/&#34;</span> <span style="color:#75715e"># [tl! .cmd:1]</span> </span></span><span style="display:flex;"><span>export VAULT_TOKEN<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;hvs.CAES[...]GSFQ&#34;</span> </span></span></code></pre></div><p>Okay, now I can run the Ubuntu 22.04 build from the top-level of my Packer directory like so:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>packer init builds/linux/ubuntu/22-04-lts <span style="color:#75715e"># [tl! .cmd:1]</span> </span></span><span style="display:flex;"><span>packer build -on-error<span style="color:#f92672">=</span>cleanup -force builds/linux/ubuntu/22-04-lts </span></span><span style="display:flex;"><span>proxmox-iso.linux-server: output will be in this color. <span style="color:#75715e"># [tl! .nocopy:start]</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#f92672">==</span>&gt; proxmox-iso.linux-server: Creating CD disk... <span style="color:#75715e"># [tl! collapse:15]</span> </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: xorriso 1.5.6 : RockRidge filesystem manipulator, libburnia project. </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: xorriso : NOTE : Environment variable SOURCE_DATE_EPOCH encountered with value <span style="color:#ae81ff">315532800</span> </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Drive current: -outdev <span style="color:#e6db74">&#39;stdio:/tmp/packer684761677.iso&#39;</span> </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Media current: stdio file, overwriteable </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Media status : is blank </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Media summary: <span style="color:#ae81ff">0</span> sessions, <span style="color:#ae81ff">0</span> data blocks, <span style="color:#ae81ff">0</span> data, 174g free </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: xorriso : WARNING : -volid text does not comply to ISO <span style="color:#ae81ff">9660</span> / ECMA <span style="color:#ae81ff">119</span> rules </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Added to ISO image: directory <span style="color:#e6db74">&#39;/&#39;</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#39;/tmp/packer_to_cdrom2909484587&#39;</span> </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: xorriso : UPDATE : <span style="color:#ae81ff">2</span> files added in <span style="color:#ae81ff">1</span> seconds </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: xorriso : UPDATE : <span style="color:#ae81ff">2</span> files added in <span style="color:#ae81ff">1</span> seconds </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: ISO image produced: <span style="color:#ae81ff">186</span> sectors </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Written to medium : <span style="color:#ae81ff">186</span> sectors at LBA <span style="color:#ae81ff">0</span> </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Writing to <span style="color:#e6db74">&#39;stdio:/tmp/packer684761677.iso&#39;</span> completed successfully. </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Done copying paths from CD_dirs </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Uploaded ISO to local:iso/packer684761677.iso </span></span><span style="display:flex;"><span><span style="color:#f92672">==</span>&gt; proxmox-iso.linux-server: Force set, checking <span style="color:#66d9ef">for</span> existing artifact on PVE cluster </span></span><span style="display:flex;"><span><span style="color:#f92672">==</span>&gt; proxmox-iso.linux-server: No existing artifact found </span></span><span style="display:flex;"><span><span style="color:#f92672">==</span>&gt; proxmox-iso.linux-server: Creating VM </span></span><span style="display:flex;"><span><span style="color:#f92672">==</span>&gt; proxmox-iso.linux-server: No VM ID given, getting next free from Proxmox </span></span><span style="display:flex;"><span><span style="color:#f92672">==</span>&gt; proxmox-iso.linux-server: Starting VM </span></span><span style="display:flex;"><span><span style="color:#f92672">==</span>&gt; proxmox-iso.linux-server: Waiting 4s <span style="color:#66d9ef">for</span> boot </span></span><span style="display:flex;"><span><span style="color:#f92672">==</span>&gt; proxmox-iso.linux-server: Typing the boot command </span></span><span style="display:flex;"><span><span style="color:#f92672">==</span>&gt; proxmox-iso.linux-server: Waiting <span style="color:#66d9ef">for</span> SSH to become available... <span style="color:#75715e"># [tl! .nocopy:end]</span> </span></span></code></pre></div><p>It'll take a few minutes while Packer waits on SSH, and while I wait on that, I can look at the Proxmox console for the VM to follow along with the installer's progress:</p> <p><img src="https://runtimeterror.dev/building-proxmox-templates-packer/proxmox-console-progress.png" alt="Proxmox VM console showing the installer progress"></p> <p>That successful SSH connection signifies the transition from the <code>source {}</code> block to the <code>build {}</code> block, so it starts with uploading any certs and the <code>join-domain.sh</code> script before getting into running those post-install tasks:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Connected to SSH! [tl! .nocopy:start **:2] </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Uploading certs =&gt; /tmp </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Uploading scripts/linux/join-domain.sh =&gt; /home/john/join-domain.sh </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: join-domain.sh 5.59 KiB / 5.59 KiB [========================================================================================================] 100.00% 0s </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Provisioning with shell script: /home/john/projects/packer-proxmox-templates/scripts/linux/wait-for-cloud-init.sh [tl! **:start] </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Waiting for cloud-init... </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Provisioning with shell script: /home/john/projects/packer-proxmox-templates/scripts/linux/cleanup-subiquity.sh </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Deleting subiquity cloud-init config... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Deleting subiquity cloud-init network config... </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Provisioning with shell script: /home/john/projects/packer-proxmox-templates/scripts/linux/install-ca-certs.sh </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Installing certificates... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: No certs to install. </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Provisioning with shell script: /home/john/projects/packer-proxmox-templates/scripts/linux/disable-multipathd.sh </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Disabling multipathd... [tl! **:end] </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Removed /etc/systemd/system/multipath-tools.service. </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Removed /etc/systemd/system/sockets.target.wants/multipathd.socket. </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Removed /etc/systemd/system/sysinit.target.wants/multipathd.service. </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Provisioning with shell script: /home/john/projects/packer-proxmox-templates/scripts/linux/prune-motd.sh [tl! **:3] </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Pruning default MOTD... </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Provisioning with shell script: /home/john/projects/packer-proxmox-templates/scripts/linux/persist-cloud-init-net.sh </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Preserving network settings... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: manual_cache_clean: True </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Provisioning with shell script: /home/john/projects/packer-proxmox-templates/scripts/linux/configure-pam_mkhomedir.sh [tl! **:3] </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Configuring pam_mkhomedir... </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Provisioning with shell script: /home/john/projects/packer-proxmox-templates/scripts/linux/update-packages.sh </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Checking for and installing updates... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Hit:1 http://security.ubuntu.com/ubuntu jammy-security InRelease </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Hit:2 http://us.archive.ubuntu.com/ubuntu jammy InRelease </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Hit:3 http://us.archive.ubuntu.com/ubuntu jammy-updates InRelease </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Hit:4 http://us.archive.ubuntu.com/ubuntu jammy-backports InRelease </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Reading package lists... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Reading package lists... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Building dependency tree... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Reading state information... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Calculating upgrade... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: The following packages have been kept back: </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: python3-update-manager update-manager-core </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: 0 upgraded, 0 newly installed, 0 to remove and 2 not upgraded. </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Rebooting! [tl! ** .nocopy:end] </span></span></code></pre></div><p>There's a brief pause during the reboot, and then things pick back up with the hardening script and then the cleanup tasks:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Pausing 30s before the next provisioner... [tl! .nocopy:start] </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Provisioning with shell script: /home/john/projects/packer-proxmox-templates/scripts/linux/cleanup-cloud-init.sh [tl! **:3] </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Cleaning up cloud-init state... </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Provisioning with shell script: /home/john/projects/packer-proxmox-templates/scripts/linux/cleanup-packages.sh </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Cleaning up unneeded packages... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Reading package lists... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Building dependency tree... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Reading state information... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: 0 upgraded, 0 newly installed, 0 to remove and 2 not upgraded. </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Provisioning with shell script: /home/john/projects/packer-proxmox-templates/builds/linux/ubuntu/22-04-lts/hardening.sh [tl! **:1] </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt;&gt; Beginning hardening tasks... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: [...] </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt;&gt; Hardening script complete! </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Provisioning with shell script: /home/john/projects/packer-proxmox-templates/scripts/linux/zero-disk.sh [tl! **:1] </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Zeroing free space to reduce disk size... </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: dd: error writing &#39;/EMPTY&#39;: No space left on device </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: 25905+0 records in </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: 25904+0 records out </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: 27162312704 bytes (27 GB, 25 GiB) copied, 10.7024 s, 2.5 GB/s </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Provisioning with shell script: /home/john/projects/packer-proxmox-templates/scripts/linux/generalize.sh [tl! **:10] </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Clearing audit logs... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Clearing persistent udev rules... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Clearing temp dirs... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Clearing host keys... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Removing Packer SSH key... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Clearing machine-id... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Clearing shell history... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: &gt;&gt; Clearing sudoers.d... </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Stopping VM </span></span><span style="display:flex;"><span>==&gt; proxmox-iso.linux-server: Converting VM to template </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Deleted generated ISO from local:iso/packer152219352.iso </span></span><span style="display:flex;"><span>Build &#39;proxmox-iso.linux-server&#39; finished after 10 minutes 52 seconds. [tl! **:5] </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>==&gt; Wait completed after 10 minutes 52 seconds </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>==&gt; Builds finished. The artifacts of successful builds are: </span></span><span style="display:flex;"><span>--&gt; proxmox-iso.linux-server: A template was created: 105 [tl! .nocopy:end] </span></span></code></pre></div><p>That was a lot of prep work, but now that everything is in place it only takes about eleven minutes to create a fresh Ubuntu 22.04 template, and that template is fully up-to-date and hardened to about 95% of the CIS Level 2 benchmark. This will save me a lot of time as I build new VMs in my homelab.</p> <h3 id="wrapper-script"> Wrapper Script <a class="hlink" href="#wrapper-script"><i class="fa-solid fa-link"></i></a> </h3><p>But having to export the Vault variables and run the Packer commands manually is a bit of a chore. So I put together a couple of helper scripts to help streamline things. This will really come in handy as I add new OS variants and schedule automated builds with GitHub Actions.</p> <p>First, I made a <code>vault-env.sh</code> script to hold my Vault address and the token for Packer.</p> <div class="notice note"> <p class="first notice-title"><span class="icon-notice baseline"></span>Sensitive Values!</p><p>The <code>VAULT_TOKEN</code> variable is a sensitive value and should be protected. This file should be added to <code>.gitignore</code> to ensure it doesn't get inadvertently committed to a repo.</p></div> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span>export VAULT_ADDR<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://vault.tailnet-name.ts.net/&#34;</span> </span></span><span style="display:flex;"><span>export VAULT_TOKEN<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;hvs.CAES[...]GSFQ&#34;</span> </span></span></code></pre></div><p>This <code>build.sh</code> script expects a single argument: the name of the build to create. It then checks to see if the <code>VAULT_TOKEN</code> environment variable is already set; if not, it tries to source it from <code>vault-env.sh</code>. And then it kicks off the appropriate build.</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># Run a single packer build</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">#</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># Specify the build as an argument to the script. Ex:</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># ./build.sh ubuntu2204</span> </span></span><span style="display:flex;"><span>set -eu </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> $# -ne <span style="color:#ae81ff">1</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;&#34;&#34; </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Syntax: </span>$0<span style="color:#e6db74"> [BUILD] </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Where [BUILD] is one of the supported OS builds: </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">ubuntu2204 ubuntu2404 </span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span> </span></span><span style="display:flex;"><span> exit <span style="color:#ae81ff">1</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> ! <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>VAULT_TOKEN+x<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> </span></span><span style="display:flex;"><span> source vault-env.sh <span style="color:#f92672">||</span> <span style="color:#f92672">(</span> echo <span style="color:#e6db74">&#34;No Vault config found&#34;</span>; exit <span style="color:#ae81ff">1</span> <span style="color:#f92672">)</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>build_name<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>1,,<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> </span></span><span style="display:flex;"><span>build_path<span style="color:#f92672">=</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">case</span> $build_name in </span></span><span style="display:flex;"><span> ubuntu2204<span style="color:#f92672">)</span> </span></span><span style="display:flex;"><span> build_path<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;builds/linux/ubuntu/22-04-lts/&#34;</span> </span></span><span style="display:flex;"><span> ;; </span></span><span style="display:flex;"><span> ubuntu2404<span style="color:#f92672">)</span> </span></span><span style="display:flex;"><span> build_path<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;builds/linux/ubuntu/24-04-lts/&#34;</span> </span></span><span style="display:flex;"><span> ;; </span></span><span style="display:flex;"><span> *<span style="color:#f92672">)</span> </span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;Unknown build; exiting...&#34;</span> </span></span><span style="display:flex;"><span> exit <span style="color:#ae81ff">1</span> </span></span><span style="display:flex;"><span> ;; </span></span><span style="display:flex;"><span><span style="color:#66d9ef">esac</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>packer init <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>build_path<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> </span></span><span style="display:flex;"><span>packer build -on-error<span style="color:#f92672">=</span>cleanup -force <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>build_path<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> </span></span></code></pre></div><p>Then I can kick off a build with just:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>./build.sh ubuntu2204 <span style="color:#75715e"># [tl! .cmd]</span> </span></span><span style="display:flex;"><span>proxmox-iso.linux-server: output will be in this color. <span style="color:#75715e"># [tl! .nocopy:6]</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#f92672">==</span>&gt; proxmox-iso.linux-server: Creating CD disk... </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: xorriso 1.5.6 : RockRidge filesystem manipulator, libburnia project. </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: xorriso : NOTE : Environment variable SOURCE_DATE_EPOCH encountered with value <span style="color:#ae81ff">315532800</span> </span></span><span style="display:flex;"><span> proxmox-iso.linux-server: Drive current: -outdev <span style="color:#e6db74">&#39;stdio:/tmp/packer2372067848.iso&#39;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>...<span style="color:#f92672">]</span> </span></span></code></pre></div><h3 id="up-next"> Up Next... <a class="hlink" href="#up-next"><i class="fa-solid fa-link"></i></a> </h3><p>Being able to generate a template on-demand is pretty cool, but the next stage of this project is to <a href="https://runtimeterror.dev/automate-packer-builds-github-actions/">integrate it with a GitHub Actions workflow</a> so that the templates can be built automatically on a schedule or as the configuration gets changed. But this post is long enough (and I've been poking at it for long enough) so that explanation will have to wait for another time.</p> <p>(If you'd like a sneak peek of what's in store, take a self-guided tour of <a href="https://github.com/jbowdre/packer-proxmox-templates" rel="external">the GitHub repo↗</a>.)</p> <p><del>Stay tuned!</del> <strong>It's here!</strong> <a href="https://runtimeterror.dev/automate-packer-builds-github-actions/">Automate Packer Builds with GitHub Actions</a></p> </description> </item> <item> <title>Kudos With Cabin</title> <link>https://runtimeterror.dev/kudos-with-cabin/</link> <pubDate>Mon, 24 Jun 2024 00:00:00 +0000</pubDate> <dc:creator>John Bowdre</dc:creator> <category>Hugo</category> <category>Javascript</category> <category>Meta</category> <category>Selfhosting</category> <guid>https://runtimeterror.dev/kudos-with-cabin/</guid><description><p>I'm not one to really worry about page view metrics, but I do like to see which of my posts attract the most attention - and where that attention might be coming from. That insight has allowed me to find new blogs and sites that have linked to mine, and has tipped me off that maybe I should update that four-year-old post that's suddenly getting renewed traffic from Reddit.</p> <p>In my quest for such knowledge, last week I switched my various web properties back to using <a href="https://withcabin.com/" rel="external">Cabin↗</a> for &quot;privacy-first, carbon conscious web analytics&quot;. I really like how lightweight and deliberately minimal Cabin is, and the portal does a great job of presenting the information that I care about. With this change, though, I gave up the cute little upvote widgets provided by the previous analytics platform.</p> <p>I recently shared <a href="https://srsbsns.lol/tracking-bear-upvotes-from-my-cabin/" rel="external">on my Bear weblog↗</a> about how I was hijacking Bear's built-in upvote button to send a &quot;kudos&quot; <a href="https://docs.withcabin.com/events.html" rel="external">event↗</a> to Cabin and tally those actions there.</p> <p>Well today I implemented a similar thing on <em>this</em> blog. Without an existing widget to hijack, I needed to create this one from scratch using a combination of HTML in my page template, CSS to style it, and JavaScript to fire the event.</p> <h3 id="layout"> Layout <a class="hlink" href="#layout"><i class="fa-solid fa-link"></i></a> </h3><p>My <a href="https://gohugo.io/" rel="external">Hugo↗</a> setup uses <code>layouts/_default/single.html</code> to control how each post is rendered, and I've already got a section at the bottom which displays the &quot;Reply by email&quot; link if replies are permitted on that page:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-html" data-lang="html"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;:true} </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;content__body&#34;</span>&gt; <span style="color:#75715e">&lt;!-- [tl! reindex(33))] --&gt;</span> </span></span><span style="display:flex;"><span> {{ .Content }} </span></span><span style="display:flex;"><span> &lt;/<span style="color:#f92672">div</span>&gt; </span></span><span style="display:flex;"><span> {{- $reply := true }} </span></span><span style="display:flex;"><span> {{- if eq .Site.Params.reply false }} </span></span><span style="display:flex;"><span> {{- $reply = false }} </span></span><span style="display:flex;"><span> {{- else if eq .Params.reply false }} </span></span><span style="display:flex;"><span> {{- $reply = false }} </span></span><span style="display:flex;"><span> {{- end }} </span></span><span style="display:flex;"><span> {{- if eq $reply true }} </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">hr</span>&gt; </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;post_email_reply&#34;</span>&gt;&lt;<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;mailto:replies@example.com?Subject=Re: {{ .Title }}&#34;</span>&gt;📧 Reply by email&lt;/<span style="color:#f92672">a</span>&gt;&lt;/<span style="color:#f92672">span</span>&gt; </span></span><span style="display:flex;"><span> {{- end }} </span></span></code></pre></div><p>I'll only want the upvote widget to appear on pages where replies are permitted so this makes a logical place to insert the new code:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-html" data-lang="html"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;:true} </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;content__body&#34;</span>&gt; <span style="color:#75715e">&lt;!-- [tl! reindex(33)] --&gt;</span> </span></span><span style="display:flex;"><span> {{ .Content }} </span></span><span style="display:flex;"><span> &lt;/<span style="color:#f92672">div</span>&gt; </span></span><span style="display:flex;"><span> {{- $reply := true }} </span></span><span style="display:flex;"><span> {{- if eq .Site.Params.reply false }} </span></span><span style="display:flex;"><span> {{- $reply = false }} </span></span><span style="display:flex;"><span> {{- else if eq .Params.reply false }} </span></span><span style="display:flex;"><span> {{- $reply = false }} </span></span><span style="display:flex;"><span> {{- end }} </span></span><span style="display:flex;"><span> {{- if eq $reply true }} </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">hr</span>&gt; </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;kudos-container&#34;</span>&gt; <span style="color:#75715e">&lt;!-- [tl! ++:5 **:5] --&gt;</span> </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;kudos-button&#34;</span>&gt; </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;emoji&#34;</span>&gt;👍&lt;/<span style="color:#f92672">span</span>&gt; </span></span><span style="display:flex;"><span> &lt;/<span style="color:#f92672">button</span>&gt; </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;kudos-text&#34;</span>&gt;Enjoyed this?&lt;/<span style="color:#f92672">span</span>&gt; </span></span><span style="display:flex;"><span> &lt;/<span style="color:#f92672">div</span>&gt; </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;post_email_reply&#34;</span>&gt;&lt;<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;mailto:replies@example.com?Subject=Re: {{ .Title }}&#34;</span>&gt;📧 Reply by email&lt;/<span style="color:#f92672">a</span>&gt;&lt;/<span style="color:#f92672">span</span>&gt; </span></span><span style="display:flex;"><span> {{- end }} </span></span></code></pre></div><p>The button won't actually do anything yet, but it'll at least appear on the page. I can use some CSS to make it look a bit nicer though.</p> <h3 id="css"> CSS <a class="hlink" href="#css"><i class="fa-solid fa-link"></i></a> </h3><p>My theme uses <code>static/css/custom.css</code> to override the defaults, so I'll drop some styling bits at the bottom of that file:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-css" data-lang="css"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">#</span> <span style="color:#f92672">torchlight</span><span style="color:#f92672">!</span> {<span style="color:#960050;background-color:#1e0010">&#34;lineNumbers&#34;:true</span>} </span></span><span style="display:flex;"><span><span style="color:#75715e">/* Cabin kudos styling [tl! reindex(406)] */</span> </span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">kudos-container</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">display</span>: <span style="color:#66d9ef">flex</span>; </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">align-items</span>: <span style="color:#66d9ef">center</span>; </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">kudos-button</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">background</span>: <span style="color:#66d9ef">none</span>; </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">border</span>: <span style="color:#66d9ef">none</span>; </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">cursor</span>: <span style="color:#66d9ef">pointer</span>; </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">font-size</span>: <span style="color:#ae81ff">1.2</span><span style="color:#66d9ef">rem</span>; </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">padding</span>: <span style="color:#ae81ff">0</span>; </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">margin-right</span>: <span style="color:#ae81ff">0.25</span><span style="color:#66d9ef">rem</span>; </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">kudos-button</span>:<span style="color:#a6e22e">disabled</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">cursor</span>: <span style="color:#66d9ef">default</span>; </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">kudos-button</span> .<span style="color:#a6e22e">emoji</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">display</span>: <span style="color:#66d9ef">inline-block</span>; </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">transition</span>: <span style="color:#66d9ef">transform</span> <span style="color:#ae81ff">0.3</span><span style="color:#66d9ef">s</span> <span style="color:#66d9ef">ease</span>; </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">kudos-button</span>.<span style="color:#a6e22e">clicked</span> .<span style="color:#a6e22e">emoji</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">transform</span>: rotate(<span style="color:#ae81ff">360</span><span style="color:#66d9ef">deg</span>); </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">kudos-text</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">transition</span>: <span style="color:#66d9ef">font-style</span> <span style="color:#ae81ff">0.3</span><span style="color:#66d9ef">s</span> <span style="color:#66d9ef">ease</span>; </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">kudos-text</span>.<span style="color:#a6e22e">thanks</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">font-style</span>: <span style="color:#66d9ef">italic</span>; </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>I got carried away a little bit and decided to add a fun animation when the button gets clicked. Which brings me to what happens when this thing gets clicked.</p> <h3 id="javascript"> JavaScript <a class="hlink" href="#javascript"><i class="fa-solid fa-link"></i></a> </h3><p>I want the button to do a little bit more than <em>just</em> send the event to Cabin so I decided to break that out into a separate script, <code>assets/js/kudos.js</code>. This script will latch on to the kudos-related elements, and when the button gets clicked it will (1) fire off the <code>cabin.event('kudos')</code> function to record the event, (2) disable the button to discourage repeated clicks, (3) change the displayed text to <code>Thanks!</code>, and (4) celebrate the event by spinning the emoji and replacing it with a party popper.</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#75715e">// torchlight! {&#34;lineNumbers&#34;:true} </span></span></span><span style="display:flex;"><span><span style="color:#75715e">// manipulates the post upvote &#34;kudos&#34; button behavior </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> </span></span><span style="display:flex;"><span>window.<span style="color:#a6e22e">onload</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">function</span>() { </span></span><span style="display:flex;"><span> <span style="color:#75715e">// get the button and text elements </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">kudosButton</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.kudos-button&#39;</span>); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">kudosText</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.kudos-text&#39;</span>); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">emojiSpan</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">kudosButton</span>.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.emoji&#39;</span>); </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">kudosButton</span>.<span style="color:#a6e22e">addEventListener</span>(<span style="color:#e6db74">&#39;click&#39;</span>, <span style="color:#66d9ef">function</span>(<span style="color:#a6e22e">event</span>) { </span></span><span style="display:flex;"><span> <span style="color:#75715e">// send the event to Cabin </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">cabin</span>.<span style="color:#a6e22e">event</span>(<span style="color:#e6db74">&#39;kudos&#39;</span>) </span></span><span style="display:flex;"><span> <span style="color:#75715e">// disable the button </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">kudosButton</span>.<span style="color:#a6e22e">disabled</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>; </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">kudosButton</span>.<span style="color:#a6e22e">classList</span>.<span style="color:#a6e22e">add</span>(<span style="color:#e6db74">&#39;clicked&#39;</span>); </span></span><span style="display:flex;"><span> <span style="color:#75715e">// change the displayed text </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">kudosText</span>.<span style="color:#a6e22e">textContent</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;Thanks!&#39;</span>; </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">kudosText</span>.<span style="color:#a6e22e">classList</span>.<span style="color:#a6e22e">add</span>(<span style="color:#e6db74">&#39;thanks&#39;</span>); </span></span><span style="display:flex;"><span> <span style="color:#75715e">// spin the emoji </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">emojiSpan</span>.<span style="color:#a6e22e">style</span>.<span style="color:#a6e22e">transform</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;rotate(360deg)&#39;</span>; </span></span><span style="display:flex;"><span> <span style="color:#75715e">// change the emoji to celebrate </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">setTimeout</span>(<span style="color:#66d9ef">function</span>() { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">emojiSpan</span>.<span style="color:#a6e22e">textContent</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;🎉&#39;</span>; </span></span><span style="display:flex;"><span> }, <span style="color:#ae81ff">150</span>); <span style="color:#75715e">// half of the css transition time for a smooth mid-rotation change </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> }); </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>The last step is to go back to my <code>single.html</code> layout and pull in this new JavaScript file. I placed it in the site's <code>assets/</code> folder so that Hugo can apply its minifying magic so I'll need to load it in as a page resource:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-html" data-lang="html"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;:true} </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;content__body&#34;</span>&gt; <span style="color:#75715e">&lt;!-- [tl! reindex(33)] --&gt;</span> </span></span><span style="display:flex;"><span> {{ .Content }} </span></span><span style="display:flex;"><span> &lt;/<span style="color:#f92672">div</span>&gt; </span></span><span style="display:flex;"><span> {{- $reply := true }} <span style="color:#75715e">&lt;!-- [tl! collapse:6] --&gt;</span> </span></span><span style="display:flex;"><span> {{- if eq .Site.Params.reply false }} </span></span><span style="display:flex;"><span> {{- $reply = false }} </span></span><span style="display:flex;"><span> {{- else if eq .Params.reply false }} </span></span><span style="display:flex;"><span> {{- $reply = false }} </span></span><span style="display:flex;"><span> {{- end }} </span></span><span style="display:flex;"><span> {{- if eq $reply true }} </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">hr</span>&gt; </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;kudos-container&#34;</span>&gt; </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;kudos-button&#34;</span>&gt; </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;emoji&#34;</span>&gt;👍&lt;/<span style="color:#f92672">span</span>&gt; </span></span><span style="display:flex;"><span> &lt;/<span style="color:#f92672">button</span>&gt; </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;kudos-text&#34;</span>&gt;Enjoyed this?&lt;/<span style="color:#f92672">span</span>&gt; </span></span><span style="display:flex;"><span> &lt;/<span style="color:#f92672">div</span>&gt; </span></span><span style="display:flex;"><span> {{ $kudos := resources.Get &#34;js/kudos.js&#34; | minify }} <span style="color:#75715e">&lt;!-- [tl! ++:1 **:1] --&gt;</span> </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">script</span> <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ $kudos.RelPermalink }}&#34;</span>&gt;&lt;/<span style="color:#f92672">script</span>&gt; </span></span><span style="display:flex;"><span> &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;post_email_reply&#34;</span>&gt;&lt;<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;mailto:replies@example.com?Subject=Re: {{ .Title }}&#34;</span>&gt;📧 Reply by email&lt;/<span style="color:#f92672">a</span>&gt;&lt;/<span style="color:#f92672">span</span>&gt; </span></span><span style="display:flex;"><span> {{- end }} </span></span></code></pre></div><p>You might have noticed that I'm not doing anything to display the upvote count on the page itself. I don't feel like the reader really needs to know how (un)popular a post may be before deciding to vote it up; the total count isn't really relevant. (Also, the Cabin stats don't update in realtime and I just didn't want to deal with that... but mostly that first bit.)</p> <p>In any case, after clicking the 👍 button on a few pages I can see the <code>kudos</code> events recorded in my <a href="https://l.runtimeterror.dev/rterror-stats" rel="external">Cabin portal↗</a>: <img src="https://runtimeterror.dev/kudos-with-cabin/kudos-in-cabin.png" alt="A few hits against the 'kudos' event"></p> <p>Go on, try it out:</p> </description> </item> <item> <title>Further Down the Bunny Hole</title> <link>https://runtimeterror.dev/further-down-the-bunny-hole/</link> <pubDate>Thu, 06 Jun 2024 00:00:00 +0000</pubDate> <dc:creator>John Bowdre</dc:creator> <category>Bunny</category> <category>Cicd</category> <category>Hugo</category> <category>Meta</category> <category>Selfhosting</category> <guid>https://runtimeterror.dev/further-down-the-bunny-hole/</guid><description><p>It wasn't too long ago (January, in fact) that I started <a href="https://runtimeterror.dev/deploy-hugo-neocities-github-actions/">hosting this site with Neocities</a>. I was pretty pleased with that setup, but a few weeks ago my <a href="https://srsbsns.lol/post/upptime-serverless-server-monitoring-c88fbaz7" rel="external">monitoring setup↗</a> started reporting that the site was down. And sure enough, trying to access the site would return a generic error message stating that the site was unknown. I eventually discovered that this was due to Neocities &quot;forgetting&quot; that the site was linked to the <code>runtimeterror.dev</code> domain. It was easy enough to just re-enter that domain in the configuration, and that immediately fixed things... until a few days later when the same thing happened again.</p> <p>The same problem has now occurred five or six times, and my messages to the Neocities support contact have gone unanswered. I didn't see anyone else online reporting this exact issue, but I found several posts on Reddit about sites getting randomly broken (or even deleted!) and support taking a week (or more) to reply. I don't have that kind of patience, so I started to consider moving my content away from Neocities and cancelling my $5/month Supporter subscription.</p> <p>I <a href="https://srsbsns.lol/post/i-just-hopped-to-bunny-net" rel="external">recently↗</a> started using <a href="https://bunny.net" rel="external">bunny.net↗</a> for the site's DNS, and had also <a href="https://runtimeterror.dev/using-custom-font-hugo/">leveraged Bunny's CDN for hosting font files</a>. This setup has been working great for me, and I realized that I could also use Bunny's CDN for hosting the entirety of my static site as well. After all, serving static files on the web is exactly what a CDN is great at. After an hour or two of tinkering, I successfully switched hosting setups with just a few seconds of downtime.</p> <p>Here's how I did it.</p> <h3 id="storage-zone"> Storage Zone <a class="hlink" href="#storage-zone"><i class="fa-solid fa-link"></i></a> </h3><p>I started by logging into Bunny's dashboard and creating a new storage zone to hold the files. I gave it a name (like <code>my-storage-zone</code>), selected the main storage region nearest to me (New York), and also enabled replication to a site in Europe and another in Asia for good measure. (That's going to cost me a whopping $0.025/GB.)</p> <p>After the storage zone was created, I clicked over to the <strong>FTP &amp; API Access</strong> page to learn how upload files. I did some initial tests with an FTP client to confirm that it worked, but mostly I just made a note of the credentials since they'll be useful later.</p> <h3 id="pull-zone"> Pull Zone <a class="hlink" href="#pull-zone"><i class="fa-solid fa-link"></i></a> </h3><p>To get files out of the storage zone and onto the web site, I had to also configure a pull zone. While looking at my new storage, I clicked <strong>Connected Pull Zones</strong> and then the <strong>+Connect Pull Zone</strong> button at the top right of the screen. From there, I selected the option to <strong>Add Pull Zone</strong> and that whisked me away to the zone creation wizard.</p> <p>Once again, I gave the zone a name (<code>my-pull-zone</code>). I left the origin settings configured to pull from my new storage zone, and also left it on the standard tier. I left all the pricing zones enabled so that the content can be served from whatever region is closest. (Even with all pricing zones activate, my delivery costs will still be just $0.01 to $0.06/GB.)</p> <p>After admiring the magnificence of my new pull zone, I clicked the menu button at the top right and select <strong>Copy Pull Zone ID</strong> and made a note of that as well.</p> <h3 id="github-action"> GitHub Action <a class="hlink" href="#github-action"><i class="fa-solid fa-link"></i></a> </h3><p>I found the <a href="https://github.com/ayeressian/bunnycdn-storage-deploy" rel="external">bunnycdn-storage-deploy↗</a> Action which makes it easy to upload content to a Bunny storage zone and also purge the cache of the pull zone at the same time. For that to work, I had to add a few new <a href="https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions" rel="external">action secrets↗</a> to my GitHub repo:</p> <table> <thead> <tr> <th>Name</th> <th>Sample Value</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td><code>BUNNY_API_KEY</code></td> <td><code>34b05c3b-[...]-1b0f8cd6f0af</code></td> <td>Used for purging the cache. Find it under <a href="https://dash.bunny.net/account/api-key" rel="external">Bunny's Account Settings↗</a></td> </tr> <tr> <td><code>BUNNY_STORAGE_ENDPOINT</code></td> <td><code>ny.storage.bunnycdn.com</code></td> <td>Hostname from the storage zone's FTP &amp; API Access page</td> </tr> <tr> <td><code>BUNNY_STORAGE_NAME</code></td> <td><code>my-storage-zone</code></td> <td>Name of the storage zone, which is also the username</td> </tr> <tr> <td><code>BUNNY_STORAGE_PASSWORD</code></td> <td><code>7cb197e5-[...]-ad35820c0de8</code></td> <td>Get it from the storage zone's FTP &amp; API Access page</td> </tr> <tr> <td><code>BUNNY_ZONE_ID</code></td> <td><code>12345</code></td> <td>The pull zone ID you copied earlier</td> </tr> </tbody> </table> <p>Then I just updated <a href="https://github.com/jbowdre/runtimeterror/blob/main/.github/workflows/deploy-prod.yml" rel="external">my deployment workflow↗</a> to swap the Bunny action in place of the Neocities one (and <a href="https://support.bunny.net/hc/en-us/articles/360000332631-How-do-I-configure-a-custom-404-page-for-my-storage-zone" rel="external">adjust the 404 page for bunny↗</a>):</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy to Production</span> <span style="color:#75715e"># [tl! collapse:start]</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># only run on changes to main</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">schedule</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f92672">cron</span>: <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">13</span> * * * </span></span><span style="display:flex;"><span> <span style="color:#f92672">workflow_dispatch</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">push</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">branches</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">main</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#f92672">concurrency</span>: <span style="color:#75715e"># prevent concurrent deploys doing strange things</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">group</span>: <span style="color:#ae81ff">deploy-prod</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">cancel-in-progress</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># Default to bash</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">defaults</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># [tl! collapse:end]</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">deploy</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build and deploy Hugo site</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">steps</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Hugo setup</span> <span style="color:#75715e"># [tl! collapse:19]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">peaceiris/actions-hugo@v2.6.0</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">with</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">hugo-version</span>: <span style="color:#e6db74">&#39;0.121.1&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">extended</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Checkout</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">with</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">submodules</span>: <span style="color:#ae81ff">recursive</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Connect to Tailscale</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">tailscale/github-action@v2</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">with</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">oauth-client-id</span>: <span style="color:#ae81ff">${{ secrets.TS_API_CLIENT_ID }}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">oauth-secret</span>: <span style="color:#ae81ff">${{ secrets.TS_API_CLIENT_SECRET }}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">tags</span>: <span style="color:#ae81ff">${{ secrets.TS_TAG }}</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configure SSH known hosts</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: |<span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> mkdir -p ~/.ssh </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> echo &#34;${{ secrets.SSH_KNOWN_HOSTS }}&#34; &gt; ~/.ssh/known_hosts </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> chmod 644 ~/.ssh/known_hosts #</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build with Hugo</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: <span style="color:#ae81ff">HUGO_REMOTE_FONT_PATH=${{ secrets.REMOTE_FONT_PATH }} hugo --minify</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Insert 404 page</span> <span style="color:#75715e"># [tl! **:4]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: <span style="color:#ae81ff">|</span> <span style="color:#75715e"># [tl! ++:1,1 --:2,1]</span> </span></span><span style="display:flex;"><span> <span style="color:#ae81ff">mkdir -p public/bunnycdn_errors</span> </span></span><span style="display:flex;"><span> <span style="color:#ae81ff">cp public/404/index.html public/not_found.html</span> </span></span><span style="display:flex;"><span> <span style="color:#ae81ff">cp public/404/index.html public/bunnycdn_errors/404.html</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Insert 404 page</span> <span style="color:#75715e"># [tl! ++:-1,1 reindex(-1)]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: |<span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> cp public/404/index.html public/not_found.html</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Highlight with Torchlight</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: |<span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> npm i @torchlight-api/torchlight-cli </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> TORCHLIGHT_TOKEN=${{ secrets.TORCHLIGHT_TOKEN }} npx torchlight</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy HTML to Neocities</span> <span style="color:#75715e"># [tl! **:5 --:5]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">bcomnes/deploy-to-neocities@v1</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">with</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">api_token</span>: <span style="color:#ae81ff">${{ secrets.NEOCITIES_API_TOKEN }}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">cleanup</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">dist_dir</span>: <span style="color:#ae81ff">public</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy to Bunny</span> <span style="color:#75715e"># [tl! **:19 ++:19 reindex(-6)]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">ayeressian/bunnycdn-storage-deploy@v2.2.2</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">with</span>: </span></span><span style="display:flex;"><span> <span style="color:#75715e"># copy from the &#39;public&#39; folder</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">source</span>: <span style="color:#ae81ff">public</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e"># to the root of the storage zone</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">destination</span>: <span style="color:#ae81ff">/</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e"># details for accessing the storage zone</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">storageZoneName</span>: <span style="color:#e6db74">&#34;${{ secrets.BUNNY_STORAGE_NAME }}&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">storagePassword</span>: <span style="color:#e6db74">&#34;${{ secrets.BUNNY_STORAGE_PASSWORD }}&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">storageEndpoint</span>: <span style="color:#e6db74">&#34;${{ secrets.BUNNY_STORAGE_ENDPOINT }}&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e"># details for accessing the pull zone</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">accessKey</span>: <span style="color:#e6db74">&#34;${{ secrets.BUNNY_API_KEY }}&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">pullZoneId</span>: <span style="color:#e6db74">&#34;${{ secrets.BUNNY_ZONE_ID }}&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e"># upload files/folders</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">upload</span>: <span style="color:#e6db74">&#34;true&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e"># remove all remote files first (clean)</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">remove</span>: <span style="color:#e6db74">&#34;true&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e"># purge the pull zone cache</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">purgePullZone</span>: <span style="color:#e6db74">&#34;true&#34;</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy GMI to Agate</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: |<span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> rsync -avz --delete --exclude=&#39;*.html&#39; --exclude=&#39;*.css&#39; --exclude=&#39;*.js&#39; -e ssh public/ deploy@${{ secrets.GMI_HOST }}:${{ secrets.GMI_CONTENT_PATH }}</span> </span></span></code></pre></div><p>After committing and pushing this change, I checked my repo on GitHub to confirm that the workflow completed successfully:</p> <p><img src="https://runtimeterror.dev/further-down-the-bunny-hole/github_success.png" alt="GitHub indicating that the &quot;Build and Deploy Prod&quot; workflow completed successfully"></p> <p>And I also checked the Bunny storage zone to confirm that the site's contents had been copied there:</p> <p><img src="https://runtimeterror.dev/further-down-the-bunny-hole/bunny_success.png" alt="Bunny dashboard page showing the site's folder structure in the storage zone"></p> <h3 id="dns"> DNS <a class="hlink" href="#dns"><i class="fa-solid fa-link"></i></a> </h3><p>With the site content in place, all remained was to switch over the DNS record. I needed to use a <code>CNAME</code> to point <code>runtimeterror.dev</code> to the new pull zone, and that meant I first had to delete the existing <code>A</code> record pointing to Neocities' IP address. I waited about thirty seconds to make sure that change really took hold before creating the new <code>CNAME</code> to link <code>runtimeterror.dev</code> to the new pull zone, <code>my-pull-zone.b-cdn.net</code>.</p> <h3 id="ssl"> SSL <a class="hlink" href="#ssl"><i class="fa-solid fa-link"></i></a> </h3><p>The final piece was to go back to the pull zone's <strong>Hostnames</strong> configuration, add <code>runtimeterror.dev</code> as a custom hostname, and allow Bunny to automatically generate and install a cert for this hostname.</p> <h3 id="all-done"> All done! <a class="hlink" href="#all-done"><i class="fa-solid fa-link"></i></a> </h3><p>That's it - everything I did to get my site shifted over to being hosted directly by Bunny. It seems quite a bit snappier to me during my initial testing, and I'm hoping that things will be a bit more stable going forward. (Even if I <em>do</em> run into any issues with this sestup, I'm pretty confident that Bunny's <a href="https://social.lol/@jbowdre/112531789177956368" rel="external">ulta-responsive support team↗</a> would be able to help me fix it.)</p> <p><em>I'm really impressed with Bunny, and honestly can't recommend their platform highly enough. If you'd like to check it out, maybe use <a href="https://bunny.net/?ref=0eh23p45xs" rel="external">this referral link↗</a>?</em></p> </description> </item> <item> <title>The Slash Page Scoop</title> <link>https://runtimeterror.dev/the-slash-page-scoop/</link> <pubDate>Sun, 02 Jun 2024 00:00:00 +0000</pubDate> <dc:creator>John Bowdre</dc:creator> <category>Hugo</category> <category>Meta</category> <guid>https://runtimeterror.dev/the-slash-page-scoop/</guid><description><p>Inspired by <a href="https://rknight.me/" rel="external">Robb Knight↗</a>'s recent <a href="https://slashpages.net/" rel="external">slash pages↗</a> site, I spent some time over the past week or two drafting some slash pages of my own.</p> <blockquote> <p>Slash pages are common pages you can add to your website, usually with a standard, root-level slug like <code>/now</code>, <code>/about</code>, or <code>/uses</code>. They tend to describe the individual behind the site and are distinguishing characteristics of the IndieWeb.</p> </blockquote> <p>On a blog that is otherwise organized in a fairly chronological manner, slash pages provide a way share information out-of-band. I think they're great for more static content (like an about page that says who I am) as well as for content that may be regularly updated (like a changelog).</p> <p>The pages that I've implemented (so far) include:</p> <ul> <li><a href="https://runtimeterror.dev/about">/about</a> tells a bit about me and my background</li> <li><a href="https://runtimeterror.dev/changelog">/changelog</a> is just <em>starting</em> to record some of visual/functional changes I make here</li> <li><a href="https://runtimeterror.dev/colophon">/colophon</a> describes the technology and services used in producing/hosting this site</li> <li><a href="https://runtimeterror.dev/homelab">/homelab</a> isn't a canonical slash page but it provides a lot of details about my homelab setup</li> <li><a href="https://runtimeterror.dev/save">/save</a> shamelessly hosts referral links for things I love and think you'll love too</li> <li><a href="https://runtimeterror.dev/uses">/uses</a> shares the stuff I use on a regular basis</li> </ul> <p>And, of course, these are collected in one place at <a href="https://runtimeterror.dev/slashes">/slashes</a>.</p> <p>Feel free to stop here if you just want to check out the slash pages, or keep on reading for some nerd stuff about how I implemented them on my Hugo site.</p> <hr> <h3 id="implementation"> Implementation <a class="hlink" href="#implementation"><i class="fa-solid fa-link"></i></a> </h3><p>All of my typical blog posts get created within the site's Hugo directory under <code>content/posts/</code>, like this one at <code>content/posts/the-slash-page-scoop/index.md</code>. They get indexed, automatically added to the list of posts on the home page, and show up in the RSS feed. I don't want my slash pages to get that treatment so I made them directly inside the <code>content</code> directory:</p> <pre tabindex="0"><code>content ├── categories ├── posts ├── search ├── 404.md ├── _index.md ├── about.md [tl! ~~] ├── changelog.md [tl! ~~] ├── colophon.md [tl! ~~] ├── homelab.md [tl! ~~] ├── save.md [tl! ~~] ├── simplex.md └── uses.md [tl! ~~] </code></pre><p>Easy enough, but I didn't then want to have to worry about manually updating a list of slash pages so I used <a href="https://gohugo.io/content-management/taxonomies/" rel="external">Hugo's Taxonomies↗</a> feature for that. I simply tagged each page with a new <code>slashes</code> category by adding it to the post's front matter:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span>--- </span></span><span style="display:flex;"><span><span style="color:#f92672">title</span>: <span style="color:#e6db74">&#34;/changelog&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">date</span>: <span style="color:#e6db74">&#34;2024-05-26&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">lastmod</span>: <span style="color:#e6db74">&#34;2024-05-30&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">description</span>: <span style="color:#e6db74">&#34;Maybe I should keep a log of all my site-related tinkering?&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">categories</span>: <span style="color:#ae81ff">slashes</span> <span style="color:#75715e"># [tl! ~~]</span> </span></span><span style="display:flex;"><span>--- </span></span></code></pre></div><div></div><div class="notice note"> <p class="first notice-title"><span class="icon-notice baseline"></span>Category Names</p><p>I really wanted to name the category <code>/slashes</code>, but that seems to trip up Hugo a bit when it comes to creating an archive of category posts. So I settled for <code>slashes</code> and came up with some workarounds to make it present the way I wanted.</p></div> <p>Hugo will automatically generate an archive page for a given taxonomy term (so a post tagged with the category <code>slashes</code> would be listed at <code>$BASE_URL/category/slashes/</code>), but I like to have a bit of control over how those archive pages are actually presented. So I create a new file at <code>content/categories/slashes/_index.md</code> and drop in this front matter:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;:true}</span> </span></span><span style="display:flex;"><span>--- </span></span><span style="display:flex;"><span><span style="color:#f92672">title</span>: <span style="color:#ae81ff">/slashes</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">url</span>: <span style="color:#ae81ff">/slashes</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">aliases</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">/categories/slashes</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">description</span>: &gt;<span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> My collection of slash pages.</span> </span></span><span style="display:flex;"><span>--- </span></span></code></pre></div><p>The <code>slashes</code> in the file path tells Hugo which taxonomy it belongs to and so it can match the appropriately-categorized posts.</p> <p>Just like with normal posts, the <code>title</code> field defines the title (duh) of the post; this way I can label the archive page as <code>/slashes</code> instead of just <code>slashes</code>.</p> <p>The <code>url</code> field lets me override where the page will be served, and I added <code>/categories/slashes</code> as an alias so that anyone who hits that canonical URL will be automatically redirected.</p> <p>Setting a <code>description</code> lets me choose what introductory text will be displayed at the top of the index page, as well as when it's shown at the next higher level archive (like <code>/categories/</code>).</p> <p>Of course, I'd like to include a link to <a href="https://slashpages.net" rel="external">slashpages.net↗</a> to provide a bit more info about what these pages are, and I can't add hyperlinks to the description text. What I <em>can</em> do is edit the template which is used for rendering the archive page. In my case, that's at <code>layouts/partials/archive.html</code>, and it starts out like this:</p> <pre tabindex="0"><code class="language-jinja-html" data-lang="jinja-html"># torchlight! {&#34;lineNumbers&#34;:true} {{ $pages := .Pages }} {{ if .IsHome }} {{ $pages = where site.RegularPages &#34;Type&#34; &#34;in&#34; site.Params.mainSections }} {{ end }} &lt;header class=&#34;content__header&#34;&gt; {{ if .IsHome }} &lt;h1&gt;{{ site.Params.indexTitle | markdownify }}&lt;/h1&gt; {{ else }} &lt;h1&gt;{{ .Title | markdownify }}{{ if eq .Kind &#34;term&#34; }}&amp;nbsp;&lt;a href=&#34;{{ .Permalink }}feed.xml&#34; aria-label=&#34;Category RSS&#34;&gt;&lt;i class=&#34;fa-solid fa-square-rss&#34;&gt;&lt;/i&gt;&lt;/a&gt;&amp;nbsp;&lt;/h1&gt; &lt;!-- [tl! ~~] --&gt; {{ with .Description }}&lt;i&gt;{{ . }}&lt;/i&gt;&lt;hr&gt;{{ else }}&lt;br&gt;{{ end }} {{ end }}{{ end }} {{ .Content }} &lt;/header&gt; </code></pre><p>Line 9 is where I had already modified the template to conditionally add an RSS link for category archive pages. I'm going to tweak the setup a bit to conditionally render designated text when the page <code>.Title</code> matches <code>/slashes</code>:</p> <pre tabindex="0"><code class="language-jinja-html" data-lang="jinja-html"># torchlight! {&#34;lineNumbers&#34;:true} {{ $pages := .Pages }} {{ if .IsHome }} {{ $pages = where site.RegularPages &#34;Type&#34; &#34;in&#34; site.Params.mainSections }} {{ end }} &lt;header class=&#34;content__header&#34;&gt; {{ if .IsHome }} &lt;h1&gt;{{ site.Params.indexTitle | markdownify }}&lt;/h1&gt; {{ else }} {{ if eq .Title &#34;/slashes&#34; }} &lt;!-- [tl! **:3 ++:3 ] --&gt; &lt;h1&gt;{{ .Title | markdownify }}&lt;/h1&gt; &lt;i&gt;My collection of &lt;a title=&#34;what&#39;s a slashpage?&#34; href=&#34;https://slashpages.net&#34;&gt;slash pages↗&lt;/a&gt;.&lt;/i&gt;&lt;hr&gt; {{ else }} &lt;h1&gt;{{ .Title | markdownify }}{{ if eq .Kind &#34;term&#34; }}&amp;nbsp;&lt;a href=&#34;{{ .Permalink }}feed.xml&#34; aria-label=&#34;Category RSS&#34;&gt;&lt;i class=&#34;fa-solid fa-square-rss&#34;&gt;&lt;/i&gt;&lt;/a&gt;&amp;nbsp;&lt;/h1&gt; {{ with .Description }}&lt;i&gt;{{ . }}&lt;/i&gt;&lt;hr&gt;{{ else }}&lt;br&gt;{{ end }} {{ end }} &lt;!-- [tl! ** ++ ] --&gt; {{ end }}{{ end }} {{ .Content }} &lt;/header&gt; </code></pre><p>So instead of rendering the <code>description</code> I defined in the front matter the archive page will show:</p> <blockquote> <p><em>My collection of <a href="https://slashpages.net" rel="external">slash pages↗</a>.</em></p> </blockquote> <p>While I'm at it, I'd like for the slash pages themselves to be listed in alphabetical order rather than sorted by date (like everything else on the site). The remainder of my <code>layouts/partials/archive.html</code> already handles a few different ways of displaying lists of content:</p> <pre tabindex="0"><code class="language-jinja-html" data-lang="jinja-html"># torchlight! {&#34;lineNumbers&#34;:true} {{- if and (eq .Kind &#34;taxonomy&#34;) (eq .Title &#34;Tags&#34;) }} &lt;!-- [tl! reindex(15)] --&gt; {{/* /tags/ */}} &lt;div class=&#34;tagsArchive&#34;&gt; {{- range $key, $value := .Site.Taxonomies }} {{- $slicedTags := ($value.ByCount) }} {{- range $slicedTags }} {{- if eq $key &#34;tags&#34;}} &lt;div&gt;&lt;a href=&#39;/{{ $key }}/{{ (replace .Name &#34;#&#34; &#34;%23&#34;) | urlize }}/&#39; title=&#34;{{ .Name }}&#34;&gt;{{ .Name }}&lt;/a&gt;&lt;sup&gt;{{ .Count }}&lt;/sup&gt;&lt;/div&gt; {{- end }} {{- end }} {{- end }} &lt;/div&gt; {{- else if eq .Kind &#34;taxonomy&#34; }} {{/* /categories/ */}} {{- $sorted := sort $pages &#34;Title&#34; }} {{- range $sorted }} {{- $postDate := .Date.Format &#34;2006-01-02&#34; }} {{- $updateDate := .Lastmod.Format &#34;2006-01-02&#34; }} &lt;article class=&#34;post&#34;&gt; &lt;header class=&#34;post__header&#34;&gt; &lt;h1&gt;&lt;a href=&#34;{{ .Permalink }}&#34;&gt;{{ .Title | markdownify }}&lt;/a&gt;&lt;/h1&gt; &lt;p class=&#34;post__meta&#34;&gt; &lt;span class=&#34;date&#34;&gt;[&#34;{{ with $updateDate }}{{ . }}{{ else }}{{ $postDate }}{{ end }}&#34;]&lt;/span&gt; &lt;/p&gt; &lt;/header&gt; &lt;section class=&#34;post__summary&#34;&gt; {{ .Description }} &lt;/section&gt; &lt;hr&gt; &lt;/article&gt; {{ end }} {{- else }} {{/* regular posts archive */}} {{- range (.Paginate $pages).Pages }} {{- $postDate := .Date.Format &#34;2006-01-02&#34; }} {{- $updateDate := .Lastmod.Format &#34;2006-01-02&#34; }} &lt;article class=&#34;post&#34;&gt; &lt;header class=&#34;post__header&#34;&gt; &lt;h1&gt;&lt;a href=&#34;{{ .Permalink }}&#34;&gt;{{ .Title | markdownify }}&lt;/a&gt;&lt;/h1&gt; &lt;p class=&#34;post__meta&#34;&gt; &lt;span class=&#34;date&#34;&gt;[&#34;{{- $postDate }}&#34;{{- if ne $postDate $updateDate }}, &#34;{{ $updateDate }}&#34;{{ end }}]&lt;/span&gt; &lt;/p&gt; &lt;/header&gt; &lt;section class=&#34;post__summary&#34;&gt; {{if .Description }}{{ .Description }}{{ else }}{{ .Summary }}{{ end }} &lt;/section&gt; &lt;hr&gt; &lt;/article&gt; {{- end }} {{- template &#34;_internal/pagination.html&#34; . }} {{- end }} </code></pre><ol> <li>The <a href="https://runtimeterror.dev/tags/">/tags/</a> archive uses a condensed display format which simply shows the tag name and the number of posts with that tag.</li> <li>Other taxonomy archives (like <a href="https://runtimeterror.dev/categories">/categories</a>) are sorted by title, displayed with a brief description, and the date that a post in the categories was published or updated.</li> <li>Archives of posts are sorted by date (most recent first) and include the post description (or summary if it doesn't have one), and both the publish and updated dates.</li> </ol> <p>I'll just tweak the second condition there to check for either a taxonomy archive or a page with the title <code>/slashes</code>:</p> <pre tabindex="0"><code class="language-jinja-html" data-lang="jinja-html"># torchlight! {&#34;lineNumbers&#34;:true} {{- if and (eq .Kind &#34;taxonomy&#34;) (eq .Title &#34;Tags&#34;) }} &lt;!-- [tl! collapse:start reindex(20)] --&gt; {{/* /tags/ */}} &lt;div class=&#34;tagsArchive&#34;&gt; {{- range $key, $value := .Site.Taxonomies }} {{- $slicedTags := ($value.ByCount) }} {{- range $slicedTags }} {{- if eq $key &#34;tags&#34;}} &lt;div&gt;&lt;a href=&#39;/{{ $key }}/{{ (replace .Name &#34;#&#34; &#34;%23&#34;) | urlize }}/&#39; title=&#34;{{ .Name }}&#34;&gt;{{ .Name }}&lt;/a&gt;&lt;sup&gt;{{ .Count }}&lt;/sup&gt;&lt;/div&gt; {{- end }} {{- end }} {{- end }} &lt;!-- [tl! collapse:end] --&gt; &lt;/div&gt; {{- else if eq .Kind &#34;taxonomy&#34; }} &lt;!-- [tl! **:3 --] --&gt; {{- else if or (eq .Kind &#34;taxonomy&#34;) (eq .Title &#34;/slashes&#34;) }} &lt;!-- [tl! ++ reindex(-1)] --&gt; {{/* /categories/ */}} &lt;!-- [tl! --] --&gt; {{/* /categories/ or /slashes/ */}} &lt;!-- [tl! ++ reindex(-1)] --&gt; {{- $sorted := sort $pages &#34;Title&#34; }} {{- range $sorted }} {{- $postDate := .Date.Format &#34;2006-01-02&#34; }} {{- $updateDate := .Lastmod.Format &#34;2006-01-02&#34; }} &lt;article class=&#34;post&#34;&gt; &lt;header class=&#34;post__header&#34;&gt; &lt;h1&gt;&lt;a href=&#34;{{ .Permalink }}&#34;&gt;{{ .Title | markdownify }}&lt;/a&gt;&lt;/h1&gt; &lt;p class=&#34;post__meta&#34;&gt; &lt;span class=&#34;date&#34;&gt;[&#34;{{ with $updateDate }}{{ . }}{{ else }}{{ $postDate }}{{ end }}&#34;]&lt;/span&gt; &lt;/p&gt; &lt;/header&gt; &lt;section class=&#34;post__summary&#34;&gt; {{ .Description }} &lt;/section&gt; &lt;hr&gt; &lt;/article&gt; {{ end }} {{- else }} &lt;!-- [tl! collapse:start] --&gt; {{/* regular posts archive */}} {{- range (.Paginate $pages).Pages }} {{- $postDate := .Date.Format &#34;2006-01-02&#34; }} {{- $updateDate := .Lastmod.Format &#34;2006-01-02&#34; }} &lt;article class=&#34;post&#34;&gt; &lt;header class=&#34;post__header&#34;&gt; &lt;h1&gt;&lt;a href=&#34;{{ .Permalink }}&#34;&gt;{{ .Title | markdownify }}&lt;/a&gt;&lt;/h1&gt; &lt;p class=&#34;post__meta&#34;&gt; &lt;span class=&#34;date&#34;&gt;[&#34;{{- $postDate }}&#34;{{- if ne $postDate $updateDate }}, &#34;{{ $updateDate }}&#34;{{ end }}]&lt;/span&gt; &lt;/p&gt; &lt;/header&gt; &lt;section class=&#34;post__summary&#34;&gt; {{if .Description }}{{ .Description }}{{ else }}{{ .Summary }}{{ end }} &lt;/section&gt; &lt;hr&gt; &lt;/article&gt; {{- end }} {{- template &#34;_internal/pagination.html&#34; . }} {{- end }} &lt;!-- [tl! collapse:end] --&gt; </code></pre><p>So that's got the <a href="https://runtimeterror.dev/slashes/">/slashes</a> page looking the way I want it to. The last tweak will be to the template I use for displaying related (ie, in the same category) posts in the sidebar. The magic for that happens in <code>layouts/partials/aside.html</code>:</p> <pre tabindex="0"><code class="language-jinja-html" data-lang="jinja-html"># torchlight! {&#34;lineNumbers&#34;:true} {{ if .Params.description }}&lt;p&gt;{{ .Params.description }}&lt;/p&gt;&lt;hr&gt;{{ end }} &lt;!-- [tl! collapse:start] --&gt; {{ if and (gt .WordCount 400 ) (gt (len .TableOfContents) 180) }} &lt;p&gt; &lt;h3&gt;On this page&lt;/h3&gt; {{ .TableOfContents }} &lt;hr&gt; &lt;/p&gt; {{ end }} &lt;!-- [tl! collapse:end] --&gt; {{ if isset .Params &#34;categories&#34; }} &lt;!--[tl! **:start] --&gt; {{$related := where .Site.RegularPages &#34;.Params.categories&#34; &#34;eq&#34; .Params.categories }} {{- $relatedLimit := default 8 .Site.Params.numberOfRelatedPosts }} {{ if eq .Params.categories &#34;slashes&#34; }} &lt;!-- [tl! ++:10] --&gt; &lt;h3&gt;More /slashes&lt;/h3&gt; {{ $sortedPosts := sort $related &#34;Title&#34; }} &lt;ul&gt; {{- range $sortedPosts }} &lt;li&gt; &lt;a href=&#34;{{ .Permalink }}&#34; title=&#34;{{ .Title }}&#34;&gt;{{ .Title | markdownify }}&lt;/a&gt; &lt;/li&gt; {{ end }} &lt;/ul&gt; {{ else }} &lt;h3&gt;More {{ .Params.categories }}&lt;/h3&gt; &lt;ul&gt; {{- range first $relatedLimit $related }} &lt;li&gt; &lt;a href=&#34;{{ .Permalink }}&#34; title=&#34;{{ .Title }}&#34;&gt;{{ .Title | markdownify }}&lt;/a&gt; &lt;/li&gt; {{ end }} {{ if gt (len $related) $relatedLimit }} &lt;li&gt; &lt;a href=&#34;/categories/{{ lower .Params.categories }}/&#34;&gt;&lt;i&gt;See all {{ .Params.categories }}&lt;/i&gt;&lt;/a&gt; &lt;/li&gt; {{ end }} &lt;/ul&gt; {{ end }} &lt;!-- [tl! ++ **:end] --&gt; &lt;hr&gt; {{ end }} {{- $posts := where .Site.RegularPages &#34;Type&#34; &#34;in&#34; .Site.Params.mainSections }} &lt;!-- [tl! collase:12] --&gt; {{- $featured := default 8 .Site.Params.numberOfFeaturedPosts }} {{- $featuredPosts := first $featured (where $posts &#34;Params.featured&#34; true)}} {{- with $featuredPosts }} &lt;h3&gt;Featured Posts&lt;/h3&gt; &lt;ul&gt; {{- range . }} &lt;li&gt; &lt;a href=&#34;{{ .Permalink }}&#34; title=&#34;{{ .Title }}&#34;&gt;{{ .Title | markdownify }}&lt;/a&gt; &lt;/li&gt; {{- end }} &lt;/ul&gt; {{- end }} </code></pre><p>So now if you visit any of my slash pages (like, say, <a href="https://runtimeterror.dev/colophon/">/colophon</a>) you'll see the alphabetized list of other slash pages in the side bar.</p> <h3 id="closing"> Closing <a class="hlink" href="#closing"><i class="fa-solid fa-link"></i></a> </h3><p>I'll probably keep tweaking these slash pages in the coming days, but for now I'm really glad to finally have them posted. I've only thinking about doing this for the past six months.</p> </description> </item> <item> <title>Prettify Hugo RSS Feeds with XSLT</title> <link>https://runtimeterror.dev/prettify-hugo-rss-feed-xslt/</link> <pubDate>Tue, 30 Apr 2024 00:00:00 +0000</pubDate> <dc:creator>John Bowdre</dc:creator> <category>Hugo</category> <category>Meta</category> <guid>https://runtimeterror.dev/prettify-hugo-rss-feed-xslt/</guid><description><p>I put in some work several months back making my sure my site's RSS would work well in a feed reader. This meant making a <em>lot</em> of modifications to the <a href="https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/_default/rss.xml" rel="external">default Hugo RSS template↗</a>. I made it load the full article text rather than just the summary, present correctly-formatted code blocks with no loss of important whitespace, include inline images, and even pass online validation checks:</p> <p><a href="http://validator.w3.org/feed/check.cgi?url=https%3A//runtimeterror.dev/feed.xml" rel="external"><img src="https://runtimeterror.dev/prettify-hugo-rss-feed-xslt/valid-rss-rogers.png" alt="Validate my RSS feed">↗</a></p> <p>But while the feed looks great when rendered by a reader, the browser presentation left some to be desired...</p> <p><img src="https://runtimeterror.dev/prettify-hugo-rss-feed-xslt/ugly-rss.png" alt="Ugly RSS rendered without styling"></p> <p>It feels like there should be a friendlier way to present a feed &quot;landing page&quot; to help users new to RSS figure out what they need to do in order to follow a blog - and there absolutely is. In much the same way that you can prettify plain HTML with the inclusion of a CSS stylesheet, you can also style boring XML using <a href="https://www.w3schools.com/xml/xsl_intro.asp" rel="external">eXtensible Stylesheet Language Transformations (XSLT)↗</a>.</p> <p>This post will quickly cover how I used XSLT to style my blog's RSS feed and made it look like this:</p> <p><img src="https://runtimeterror.dev/prettify-hugo-rss-feed-xslt/pretty-feed.png" alt="Much more attractive RSS feed with styling to fit the site's theme"></p> <h3 id="starting-point"> Starting Point <a class="hlink" href="#starting-point"><i class="fa-solid fa-link"></i></a> </h3><p>The <a href="https://gohugo.io/templates/rss/" rel="external">RSS Templates↗</a> page from the Hugo documentation site provides some basic information about how to generate (and customize) an RSS feed for a Hugo-powered site. The basic steps are to <a href="https://github.com/jbowdre/runtimeterror/blob/871be9794234177c1bfa0b1c470873bde8f046be/config/_default/hugo.toml#L19-L30" rel="external">enable the RSS output in <code>hugo.toml</code>↗</a>, include a link to the generated feed inside the <code>&lt;head&gt;</code> element of the site template (I added it to <a href="https://github.com/jbowdre/runtimeterror/blob/871be9794234177c1bfa0b1c470873bde8f046be/layouts/partials/head.html#L8-L11" rel="external"><code>layouts/partials/head.html</code>↗</a>), and (optionally) include a customized RSS template to influence how the output gets rendered.</p> <p>Here's the content of my <code>layouts/_default/rss.xml</code>, before adding the XSLT styling:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;: true} </span></span><span style="display:flex;"><span>{{- $pctx := . -}} </span></span><span style="display:flex;"><span>{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}} </span></span><span style="display:flex;"><span>{{- $pages := slice -}} </span></span><span style="display:flex;"><span>{{- if or $.IsHome $.IsSection -}} </span></span><span style="display:flex;"><span>{{- $pages = (where $pctx.RegularPages &#34;Type&#34; &#34;in&#34; site.Params.mainSections) -}} </span></span><span style="display:flex;"><span>{{- else -}} </span></span><span style="display:flex;"><span>{{- $pages = (where $pctx.Pages &#34;Type&#34; &#34;in&#34; site.Params.mainSections) -}} </span></span><span style="display:flex;"><span>{{- end -}} </span></span><span style="display:flex;"><span>{{- $limit := .Site.Config.Services.RSS.Limit -}} </span></span><span style="display:flex;"><span>{{- if ge $limit 1 -}} </span></span><span style="display:flex;"><span>{{- $pages = $pages | first $limit -}} </span></span><span style="display:flex;"><span>{{- end -}} </span></span><span style="display:flex;"><span>{{- printf &#34;<span style="color:#75715e">&lt;?xml version=\&#34;1.0\&#34; encoding=\&#34;utf-8\&#34; standalone=\&#34;yes\&#34;?&gt;</span>&#34; | safeHTML }} </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;rss</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#34;2.0&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">xmlns:atom=</span><span style="color:#e6db74">&#34;http://www.w3.org/2005/Atom&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">xmlns:content=</span><span style="color:#e6db74">&#34;http://purl.org/rss/1.0/modules/content/&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">xmlns:dc=</span><span style="color:#e6db74">&#34;http://purl.org/dc/elements/1.1/&#34;</span><span style="color:#f92672">&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;channel&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;title&gt;</span>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}<span style="color:#f92672">&lt;/title&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;link&gt;</span>{{ .Permalink }}<span style="color:#f92672">&lt;/link&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;description&gt;</span>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}<span style="color:#f92672">&lt;/description&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;generator&gt;</span>Hugo -- gohugo.io<span style="color:#f92672">&lt;/generator&gt;</span>{{ with .Site.LanguageCode }} </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;language&gt;</span>{{.}}<span style="color:#f92672">&lt;/language&gt;</span>{{end}}{{ with .Site.Copyright }} </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;copyright&gt;</span>{{.}}<span style="color:#f92672">&lt;/copyright&gt;</span>{{end}}{{ if not .Date.IsZero }} </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;lastBuildDate&gt;</span>{{ .Date.Format &#34;Mon, 02 Jan 2006 15:04:05 -0700&#34; | safeHTML }}<span style="color:#f92672">&lt;/lastBuildDate&gt;</span>{{ end }} </span></span><span style="display:flex;"><span> {{- with .OutputFormats.Get &#34;RSS&#34; -}} </span></span><span style="display:flex;"><span> {{ printf &#34;<span style="color:#f92672">&lt;atom:link</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">%q</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">\&#34;self\&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">%q</span> <span style="color:#f92672">/&gt;</span>&#34; .Permalink .MediaType | safeHTML }} </span></span><span style="display:flex;"><span> {{- end -}} </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;image&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;url&gt;</span>{{ .Site.Params.fallBackOgImage | absURL }}<span style="color:#f92672">&lt;/url&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;title&gt;</span>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}<span style="color:#f92672">&lt;/title&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;link&gt;</span>{{ .Permalink }}<span style="color:#f92672">&lt;/link&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;/image&gt;</span> </span></span><span style="display:flex;"><span> {{ range $pages }} </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;item&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;title&gt;</span>{{ .Title | plainify }}<span style="color:#f92672">&lt;/title&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;link&gt;</span>{{ .Permalink }}<span style="color:#f92672">&lt;/link&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;pubDate&gt;</span>{{ .Date.Format &#34;Mon, 02 Jan 2006 15:04:05 -0700&#34; | safeHTML }}<span style="color:#f92672">&lt;/pubDate&gt;</span> </span></span><span style="display:flex;"><span> {{ with .Site.Params.Author.name }}<span style="color:#f92672">&lt;dc:creator&gt;</span>{{.}}<span style="color:#f92672">&lt;/dc:creator&gt;</span>{{ end }} </span></span><span style="display:flex;"><span> {{ with .Params.series }}<span style="color:#f92672">&lt;category&gt;</span>{{ . | lower }}<span style="color:#f92672">&lt;/category&gt;</span>{{ end }} </span></span><span style="display:flex;"><span> {{ range (.GetTerms &#34;tags&#34;) }} </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;category&gt;</span>{{ .LinkTitle }}<span style="color:#f92672">&lt;/category&gt;</span>{{ end }} </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;guid&gt;</span>{{ .Permalink }}<span style="color:#f92672">&lt;/guid&gt;</span> </span></span><span style="display:flex;"><span> {{- $content := replaceRE &#34;a href=\&#34;(#.*?)\&#34;&#34; (printf &#34;%s%s%s&#34; &#34;a href=\&#34;&#34; .Permalink &#34;$1\&#34;&#34;) .Content -}} </span></span><span style="display:flex;"><span> {{- $content = replaceRE &#34;img src=\&#34;(.*?)\&#34;&#34; (printf &#34;%s%s%s&#34; &#34;img src=\&#34;&#34; .Permalink &#34;$1\&#34;&#34;) $content -}} </span></span><span style="display:flex;"><span> {{- $content = replaceRE &#34;<span style="color:#f92672">&lt;svg.</span><span style="color:#960050;background-color:#1e0010">*&lt;/svg</span><span style="color:#f92672">&gt;</span>&#34; &#34;&#34; $content -}} </span></span><span style="display:flex;"><span> {{- $content = replaceRE `-moz-tab-size:\d;-o-tab-size:\d;tab-size:\d;?` &#34;&#34; $content -}} </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;description&gt;</span>{{ $content | html }}<span style="color:#f92672">&lt;/description&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;/item&gt;</span> </span></span><span style="display:flex;"><span> {{ end }} </span></span><span style="display:flex;"><span> <span style="color:#f92672">&lt;/channel&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/rss&gt;</span> </span></span></code></pre></div><p>There's a lot going on here, but much of it is conditional logic so that Hugo can use the same template to render feeds for individual tags, categories, or the entire site. It then loops through the <code>range</code> of pages of that type to generate the data for each post. It also uses the <a href="https://gohugo.io/functions/strings/replacere/" rel="external">Hugo <code>strings.ReplaceRE</code> function↗</a> to replace relative image and anchor links with the full paths so those references will work correctly in readers, and to clean up some potentially-problematic HTML markup that was causing validation failures.</p> <p>All I really need to do to get this XML ready to be styled is just link in a style sheet, and I'll do that by inserting a <code>&lt;?xml-stylesheet /&gt;</code> element directly below the top-level <code>&lt;?xml /&gt;</code> one:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;: true, &#34;lineNumbersStart&#34;: 10} </span></span><span style="display:flex;"><span>{{- if ge $limit 1 -}} </span></span><span style="display:flex;"><span>{{- $pages = $pages | first $limit -}} </span></span><span style="display:flex;"><span>{{- end -}} </span></span><span style="display:flex;"><span>{{- printf &#34;<span style="color:#75715e">&lt;?xml version=\&#34;1.0\&#34; encoding=\&#34;utf-8\&#34; standalone=\&#34;yes\&#34;?&gt;</span>&#34; | safeHTML }} </span></span><span style="display:flex;"><span>{{ printf &#34;<span style="color:#75715e">&lt;?xml-stylesheet type=\&#34;text/xsl\&#34; href=\&#34;/xml/feed.xsl\&#34; media=\&#34;all\&#34;?&gt;</span>&#34; | safeHTML }} <span style="color:#75715e">&lt;!-- [tl! ++ ] --&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;rss</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#34;2.0&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">xmlns:atom=</span><span style="color:#e6db74">&#34;http://www.w3.org/2005/Atom&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">xmlns:content=</span><span style="color:#e6db74">&#34;http://purl.org/rss/1.0/modules/content/&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">xmlns:dc=</span><span style="color:#e6db74">&#34;http://purl.org/dc/elements/1.1/&#34;</span><span style="color:#f92672">&gt;</span> </span></span></code></pre></div><p>I'll put the stylesheet in <code>static/xml/feed.xsl</code> so Hugo will make it available at the appropriate path.</p> <h3 id="creating-the-style"> Creating the Style <a class="hlink" href="#creating-the-style"><i class="fa-solid fa-link"></i></a> </h3><p>While trying to figure out how I could dress up my RSS XML, I came across the <a href="https://github.com/getnikola/nikola/blob/master/nikola/data/themes/base/assets/xml/rss.xsl" rel="external">default XSL file↗</a> provided with the <a href="https://getnikola.com/" rel="external">Nikola SSG↗</a>, and I thought it looked like a pretty good starting point.</p> <p>Here's Nikola's default XSL:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;: true} </span></span><span style="display:flex;"><span><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;xsl:stylesheet</span> <span style="color:#a6e22e">xmlns:xsl=</span><span style="color:#e6db74">&#34;http://www.w3.org/1999/XSL/Transform&#34;</span> <span style="color:#a6e22e">xmlns:atom=</span><span style="color:#e6db74">&#34;http://www.w3.org/2005/Atom&#34;</span> <span style="color:#a6e22e">xmlns:dc=</span><span style="color:#e6db74">&#34;http://purl.org/dc/elements/1.1/&#34;</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#34;1.0&#34;</span><span style="color:#f92672">&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;xsl:output</span> <span style="color:#a6e22e">method=</span><span style="color:#e6db74">&#34;xml&#34;</span><span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;xsl:template</span> <span style="color:#a6e22e">match=</span><span style="color:#e6db74">&#34;/&#34;</span><span style="color:#f92672">&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;html</span> <span style="color:#a6e22e">xmlns=</span><span style="color:#e6db74">&#34;http://www.w3.org/1999/xhtml&#34;</span> <span style="color:#a6e22e">lang=</span><span style="color:#e6db74">&#34;en&#34;</span><span style="color:#f92672">&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;head&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;meta</span> <span style="color:#a6e22e">charset=</span><span style="color:#e6db74">&#34;UTF-8&#34;</span><span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;meta</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;viewport&#34;</span> <span style="color:#a6e22e">content=</span><span style="color:#e6db74">&#34;width=device-width&#34;</span><span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;title&gt;&lt;xsl:value-of</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;rss/channel/title&#34;</span><span style="color:#f92672">/&gt;</span> (RSS)<span style="color:#f92672">&lt;/title&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;style&gt;</span><span style="color:#75715e">&lt;![CDATA[html{margin:0;padding:0;}body{color:hsl(180,1%,31%);font-family:Helvetica,Arial,sans-serif;font-size:17px;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:20rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}ol{list-style-type:disc;padding-left:1rem;}h2{font-size:22px;font-weight:inherit;}]]&gt;</span><span style="color:#f92672">&lt;/style&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/head&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;body&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;h1&gt;&lt;xsl:value-of</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;rss/channel/title&#34;</span><span style="color:#f92672">/&gt;</span> (RSS)<span style="color:#f92672">&lt;/h1&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;p&gt;</span>This is an <span style="color:#f92672">&lt;abbr</span> <span style="color:#a6e22e">title=</span><span style="color:#e6db74">&#34;Really Simple Syndication&#34;</span><span style="color:#f92672">&gt;</span>RSS<span style="color:#f92672">&lt;/abbr&gt;</span> feed. To subscribe to it, copy its address and paste it when your feed reader asks for it. It will be updated periodically in your reader. New to feeds? <span style="color:#f92672">&lt;a</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;https://duckduckgo.com/?q=how+to+get+started+with+rss+feeds&#34;</span> <span style="color:#a6e22e">title=</span><span style="color:#e6db74">&#34;Search on the web to learn more&#34;</span><span style="color:#f92672">&gt;</span>Learn more<span style="color:#f92672">&lt;/a&gt;</span>.<span style="color:#f92672">&lt;/p&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;p&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;label</span> <span style="color:#a6e22e">for=</span><span style="color:#e6db74">&#34;address&#34;</span><span style="color:#f92672">&gt;</span>RSS address:<span style="color:#f92672">&lt;/label&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;input&gt;&lt;xsl:attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;type&#34;</span><span style="color:#f92672">&gt;</span>url<span style="color:#f92672">&lt;/xsl:attribute&gt;&lt;xsl:attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;id&#34;</span><span style="color:#f92672">&gt;</span>address<span style="color:#f92672">&lt;/xsl:attribute&gt;&lt;xsl:attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;spellcheck&#34;</span><span style="color:#f92672">&gt;</span>false<span style="color:#f92672">&lt;/xsl:attribute&gt;&lt;xsl:attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;value&#34;</span><span style="color:#f92672">&gt;&lt;xsl:value-of</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;rss/channel/atom:link[@rel=&#39;self&#39;]/@href&#34;</span><span style="color:#f92672">/&gt;&lt;/xsl:attribute&gt;&lt;/input&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/p&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;p&gt;</span>Preview of the feed’s current headlines:<span style="color:#f92672">&lt;/p&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;ol&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;xsl:for-each</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;rss/channel/item&#34;</span><span style="color:#f92672">&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;li&gt;&lt;h2&gt;&lt;a&gt;&lt;xsl:attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;href&#34;</span><span style="color:#f92672">&gt;&lt;xsl:value-of</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;link&#34;</span><span style="color:#f92672">/&gt;&lt;/xsl:attribute&gt;&lt;xsl:value-of</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;title&#34;</span><span style="color:#f92672">/&gt;&lt;/a&gt;&lt;/h2&gt;&lt;/li&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/xsl:for-each&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/ol&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/body&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/html&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/xsl:template&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/xsl:stylesheet&gt;</span> </span></span></code></pre></div><p>If I just plug that in at <code>static/xml/feed.xml</code>, I do successfully get a styled (though <em>very</em> white) RSS page:</p> <p><img src="https://runtimeterror.dev/prettify-hugo-rss-feed-xslt/very-white-feed.png" alt="A very bright white (but styled) RSS page"></p> <p>I'd like this to inherit the same styling as the rest of the site so that it looks like it belongs. I can go a long way toward that by bringing in the CSS stylesheets that are used on every page, and I'll also tweak the existing <code>&lt;style /&gt;</code> element to remove some conflicts with my preferred styling:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;: true, &#34;lineNumbersStart&#34;: 10} </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;title&gt;&lt;xsl:value-of</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;rss/channel/title&#34;</span><span style="color:#f92672">/&gt;</span> (RSS)<span style="color:#f92672">&lt;/title&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;style&gt;</span><span style="color:#75715e">&lt;![CDATA[html{margin:0;padding:0;}body{color:hsl(180,1%,31%);font-family:Helvetica,Arial,sans-serif;font-size:17px;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:20rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}ol{list-style-type:disc;padding-left:1rem;}h2{font-size:22px;font-weight:inherit;}]]&gt;</span><span style="color:#f92672">&lt;/style&gt;&lt;title&gt;&lt;xsl:value-of</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;rss/channel/title&#34;</span><span style="color:#f92672">/&gt;</span> (RSS)<span style="color:#f92672">&lt;/title&gt;</span> <span style="color:#75715e">&lt;!-- [tl! remove ] --&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;style&gt;</span><span style="color:#75715e">&lt;![CDATA[html{margin:0;padding:0;}body{color:font-size:1.1rem;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:20rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}h2{font-size:22px;font-weight:inherit;}h2:before{content:&#34;&#34; !important;}]]&gt;</span><span style="color:#f92672">&lt;/style&gt;</span> <span style="color:#75715e">&lt;!-- [tl! ++:3 reindex(-1) ] --&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;stylesheet&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/css/palettes/runtimeterror.css&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;stylesheet&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/css/risotto.css&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;stylesheet&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/css/custom.css&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/head&gt;</span> </span></span></code></pre></div><p>While I'm at it, I'll also go on and add in some favicons:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;: true, &#34;lineNumbersStart&#34;: 10} </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;title&gt;&lt;xsl:value-of</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;rss/channel/title&#34;</span><span style="color:#f92672">/&gt;</span> (RSS)<span style="color:#f92672">&lt;/title&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;style&gt;</span><span style="color:#75715e">&lt;![CDATA[html{margin:0;padding:0;}body{color:font-size:1.1rem;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:20rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}h2{font-size:22px;font-weight:inherit;}h2:before{content:&#34;&#34; !important;}]]&gt;</span><span style="color:#f92672">&lt;/style&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;apple-touch-icon&#34;</span> <span style="color:#a6e22e">sizes=</span><span style="color:#e6db74">&#34;180x180&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/icons/apple-touch-icon.png&#34;</span> <span style="color:#f92672">/&gt;</span> <span style="color:#75715e">&lt;!-- [tl! ++:5] --&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;icon&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;image/png&#34;</span> <span style="color:#a6e22e">sizes=</span><span style="color:#e6db74">&#34;32x32&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/icons/favicon-32x32.png&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;icon&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;image/png&#34;</span> <span style="color:#a6e22e">sizes=</span><span style="color:#e6db74">&#34;16x16&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/icons/favicon-16x16.png&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;manifest&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/icons/site.webmanifest&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;mask-icon&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/icons/safari-pinned-tab.svg&#34;</span> <span style="color:#a6e22e">color=</span><span style="color:#e6db74">&#34;#5bbad5&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;shortcut icon&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/icons/favicon.ico&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;stylesheet&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/css/palettes/runtimeterror.css&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;stylesheet&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/css/risotto.css&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;stylesheet&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/css/custom.css&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span></code></pre></div><p>That's getting there:</p> <p><img src="https://runtimeterror.dev/prettify-hugo-rss-feed-xslt/getting-there-feed.png" alt="A darker styled RSS page"></p> <p>Including those CSS styles means that the rendered page now uses my color palette and the <a href="https://runtimeterror.dev/using-custom-font-hugo/">font I worked so hard to integrate</a>. I'm just going to make a few more tweaks to change some of the formatting, put the <code>New to feeds?</code> bit on its own line, and point to <a href="https://mojeek.com" rel="external">Mojeek↗</a> instead of DDG (<a href="https://srsbsns.lol/post/a-comprehensive-evaluation-of-various-search-engines-i-ve-used" rel="external">why?↗</a>).</p> <p>Here's my final (for now) <code>static/xml/feed.xsl</code> file:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span># torchlight! {&#34;lineNumbers&#34;: true} </span></span><span style="display:flex;"><span><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">&lt;!-- adapted from https://github.com/getnikola/nikola/blob/master/nikola/data/themes/base/assets/xml/rss.xsl --&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;xsl:stylesheet</span> <span style="color:#a6e22e">xmlns:xsl=</span><span style="color:#e6db74">&#34;http://www.w3.org/1999/XSL/Transform&#34;</span> <span style="color:#a6e22e">xmlns:atom=</span><span style="color:#e6db74">&#34;http://www.w3.org/2005/Atom&#34;</span> <span style="color:#a6e22e">xmlns:dc=</span><span style="color:#e6db74">&#34;http://purl.org/dc/elements/1.1/&#34;</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#34;1.0&#34;</span><span style="color:#f92672">&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;xsl:output</span> <span style="color:#a6e22e">method=</span><span style="color:#e6db74">&#34;xml&#34;</span><span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;xsl:template</span> <span style="color:#a6e22e">match=</span><span style="color:#e6db74">&#34;/&#34;</span><span style="color:#f92672">&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;html</span> <span style="color:#a6e22e">xmlns=</span><span style="color:#e6db74">&#34;http://www.w3.org/1999/xhtml&#34;</span> <span style="color:#a6e22e">lang=</span><span style="color:#e6db74">&#34;en&#34;</span><span style="color:#f92672">&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;head&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;meta</span> <span style="color:#a6e22e">charset=</span><span style="color:#e6db74">&#34;UTF-8&#34;</span><span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;meta</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;viewport&#34;</span> <span style="color:#a6e22e">content=</span><span style="color:#e6db74">&#34;width=device-width&#34;</span><span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;title&gt;&lt;xsl:value-of</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;rss/channel/title&#34;</span><span style="color:#f92672">/&gt;</span> (RSS)<span style="color:#f92672">&lt;/title&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;style&gt;</span><span style="color:#75715e">&lt;![CDATA[html{margin:0;padding:0;}body{color:font-size:1.1rem;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:20rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}h2{font-size:22px;font-weight:inherit;}h3:before{content:&#34;&#34; !important;}]]&gt;</span><span style="color:#f92672">&lt;/style&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;apple-touch-icon&#34;</span> <span style="color:#a6e22e">sizes=</span><span style="color:#e6db74">&#34;180x180&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/icons/apple-touch-icon.png&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;icon&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;image/png&#34;</span> <span style="color:#a6e22e">sizes=</span><span style="color:#e6db74">&#34;32x32&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/icons/favicon-32x32.png&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;icon&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;image/png&#34;</span> <span style="color:#a6e22e">sizes=</span><span style="color:#e6db74">&#34;16x16&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/icons/favicon-16x16.png&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;manifest&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/icons/site.webmanifest&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;mask-icon&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/icons/safari-pinned-tab.svg&#34;</span> <span style="color:#a6e22e">color=</span><span style="color:#e6db74">&#34;#5bbad5&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;shortcut icon&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/icons/favicon.ico&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;stylesheet&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/css/palettes/runtimeterror.css&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;stylesheet&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/css/risotto.css&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;link</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">&#34;stylesheet&#34;</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;/css/custom.css&#34;</span> <span style="color:#f92672">/&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/head&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;body&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;h1&gt;&lt;xsl:value-of</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;rss/channel/title&#34;</span><span style="color:#f92672">/&gt;</span> (RSS)<span style="color:#f92672">&lt;/h1&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;p&gt;</span>This is an <span style="color:#f92672">&lt;abbr</span> <span style="color:#a6e22e">title=</span><span style="color:#e6db74">&#34;Really Simple Syndication&#34;</span><span style="color:#f92672">&gt;</span>RSS<span style="color:#f92672">&lt;/abbr&gt;</span> feed. To subscribe to it, copy its address and paste it when your feed reader asks for it. It will be updated periodically in your reader.<span style="color:#f92672">&lt;/p&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;p&gt;</span>New to feeds? <span style="color:#f92672">&lt;a</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">&#34;https://www.mojeek.com/search?q=how+to+get+started+with+rss+feeds&#34;</span> <span style="color:#a6e22e">title=</span><span style="color:#e6db74">&#34;Search on the web to learn more&#34;</span><span style="color:#f92672">&gt;</span>Learn more<span style="color:#f92672">&lt;/a&gt;</span>.<span style="color:#f92672">&lt;/p&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;p&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;label</span> <span style="color:#a6e22e">for=</span><span style="color:#e6db74">&#34;address&#34;</span><span style="color:#f92672">&gt;</span>RSS address:<span style="color:#f92672">&lt;/label&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;input&gt;&lt;xsl:attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;type&#34;</span><span style="color:#f92672">&gt;</span>url<span style="color:#f92672">&lt;/xsl:attribute&gt;&lt;xsl:attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;id&#34;</span><span style="color:#f92672">&gt;</span>address<span style="color:#f92672">&lt;/xsl:attribute&gt;&lt;xsl:attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;spellcheck&#34;</span><span style="color:#f92672">&gt;</span>false<span style="color:#f92672">&lt;/xsl:attribute&gt;&lt;xsl:attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;value&#34;</span><span style="color:#f92672">&gt;&lt;xsl:value-of</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;rss/channel/atom:link[@rel=&#39;self&#39;]/@href&#34;</span><span style="color:#f92672">/&gt;&lt;/xsl:attribute&gt;&lt;/input&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/p&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;p&gt;&lt;h2&gt;</span>Recent posts:<span style="color:#f92672">&lt;/h2&gt;&lt;/p&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;ul&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;xsl:for-each</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;rss/channel/item&#34;</span><span style="color:#f92672">&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;li&gt;&lt;h3&gt;&lt;a&gt;&lt;xsl:attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;href&#34;</span><span style="color:#f92672">&gt;&lt;xsl:value-of</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;link&#34;</span><span style="color:#f92672">/&gt;&lt;/xsl:attribute&gt;&lt;xsl:value-of</span> <span style="color:#a6e22e">select=</span><span style="color:#e6db74">&#34;title&#34;</span><span style="color:#f92672">/&gt;&lt;/a&gt;&lt;/h3&gt;&lt;/li&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/xsl:for-each&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/ul&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/body&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/html&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/xsl:template&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/xsl:stylesheet&gt;</span> </span></span></code></pre></div><p>I'm pretty pleased with <a href="https://runtimeterror.dev/feed.xml">that result</a>!</p> </description> </item> <item> <title>Using a Custom Font with Hugo</title> <link>https://runtimeterror.dev/using-custom-font-hugo/</link> <pubDate>Sun, 28 Apr 2024 00:00:00 +0000</pubDate> <dc:creator>John Bowdre</dc:creator> <category>Bunny</category> <category>Cloudflare</category> <category>Hugo</category> <category>Meta</category> <category>Tailscale</category> <guid>https://runtimeterror.dev/using-custom-font-hugo/</guid><description><p>Last week, I came across and immediately fell in love with a delightfully-retro monospace font called <a href="https://berkeleygraphics.com/typefaces/berkeley-mono/" rel="external">Berkeley Mono↗</a>. I promptly purchased a &quot;personal developer&quot; license and set to work <a href="https://srsbsns.lol/post/trying-tabby-terminal" rel="external">applying the font in my IDE and terminal↗</a>. I didn't want to stop there, though; the license also permits me to use the font on my personal site, and Berkeley Mono will fit in beautifully with the whole runtimeterror aesthetic.</p> <p>Well, you're looking at the slick new font here, and I'm about to tell you how I added the font both to the site itself and to the <a href="https://runtimeterror.dev/dynamic-opengraph-images-with-hugo/">dynamically-generated OpenGraph share images</a> setup. It wasn't terribly hard to implement, but the Hugo documentation is a bit light on how to do it (and I'm kind of inept at this whole web development thing).</p> <h3 id="web-font"> Web Font <a class="hlink" href="#web-font"><i class="fa-solid fa-link"></i></a> </h3><p>This site's styling is based on the <a href="https://github.com/joeroe/risotto/tree/main" rel="external">risotto theme for Hugo↗</a>. Risotto uses the CSS variable <code>--font-monospace</code> in <code>themes/risotto/static/css/typography.css</code> to define the font face, and then that variable is inserted wherever the font may need to be set:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-css" data-lang="css"><span style="display:flex;"><span><span style="color:#75715e">/* torchlight! {&#34;lineNumbers&#34;:true} */</span> </span></span><span style="display:flex;"><span><span style="color:#75715e">/* Fonts */</span> </span></span><span style="display:flex;"><span>:<span style="color:#a6e22e">root</span> { </span></span><span style="display:flex;"><span> --font-monospace: <span style="color:#e6db74">&#34;Fira Mono&#34;</span>, <span style="color:#66d9ef">monospace</span>; <span style="color:#75715e">/* [tl! **] */</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#f92672">body</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">font-family</span>: <span style="color:#a6e22e">var</span>(<span style="color:#f92672">--</span>font<span style="color:#f92672">-</span><span style="color:#66d9ef">monospace</span>); <span style="color:#75715e">/* [tl! **] */</span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">font-size</span>: <span style="color:#ae81ff">16</span><span style="color:#66d9ef">px</span>; </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">line-height</span>: <span style="color:#ae81ff">1.5</span><span style="color:#66d9ef">rem</span>; </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>This makes it easy to override the theme's font by inserting my preferred font in <code>static/custom.css</code>:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-css" data-lang="css"><span style="display:flex;"><span><span style="color:#75715e">/* font overrides */</span> </span></span><span style="display:flex;"><span>:<span style="color:#a6e22e">root</span> { </span></span><span style="display:flex;"><span> --font-monospace: <span style="color:#e6db74">&#39;Berkeley Mono&#39;</span>, <span style="color:#e6db74">&#39;Fira Mono&#39;</span>, <span style="color:#66d9ef">monospace</span>; <span style="color:#75715e">/* [tl! **] */</span> </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>And that would be the end of things if I could expect that everyone who visited my site already had the Berkeley Mono font installed; if they don't, though, the site will fallback to either the Fira Mono font or whatever generic monospace font is on the system. So maybe I'll add a few other monospace fonts just for good measure:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-css" data-lang="css"><span style="display:flex;"><span><span style="color:#75715e">/* font overrides */</span> </span></span><span style="display:flex;"><span>:<span style="color:#a6e22e">root</span> { </span></span><span style="display:flex;"><span> --font-monospace: <span style="color:#e6db74">&#39;Berkeley Mono&#39;</span>, <span style="color:#e6db74">&#39;IBM Plex Mono&#39;</span>, <span style="color:#e6db74">&#39;Cascadia Mono&#39;</span>, <span style="color:#e6db74">&#39;Roboto Mono&#39;</span>, <span style="color:#e6db74">&#39;Source Code Pro&#39;</span>, <span style="color:#e6db74">&#39;Fira Mono&#39;</span>, <span style="color:#e6db74">&#39;Courier New&#39;</span>, <span style="color:#66d9ef">monospace</span>; <span style="color:#75715e">/* [tl! **] */</span> </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>That provides a few more options to fall back to if the preferred font isn't available. But let's see about making that font available.</p> <h4 id="hosted-locally"> Hosted Locally <a class="hlink" href="#hosted-locally"><i class="fa-solid fa-link"></i></a> </h4><p>I can use a <code>@font-face</code> rule to tell the browser how to find the <code>.woff2</code> file for my preferred web font, and I could just set the <code>src: url</code> parameter to point to a local path in my Hugo environment:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-css" data-lang="css"><span style="display:flex;"><span><span style="color:#75715e">/* load preferred font */</span> </span></span><span style="display:flex;"><span>@<span style="color:#66d9ef">font-face</span> { </span></span><span style="display:flex;"><span> <span style="color:#f92672">font-family</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Berkeley Mono&#39;</span><span style="color:#f92672">;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">font-style</span><span style="color:#f92672">:</span> <span style="color:#f92672">normal</span><span style="color:#f92672">;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">font-weight</span><span style="color:#f92672">:</span> <span style="color:#f92672">400</span><span style="color:#f92672">;</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">/* use the installed font with this name if it&#39;s there... */</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">src</span><span style="color:#f92672">:</span> <span style="color:#f92672">local</span><span style="color:#f92672">(</span><span style="color:#e6db74">&#39;Berkeley Mono&#39;</span><span style="color:#f92672">),</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">/* otherwise look at these paths */</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">url</span><span style="color:#f92672">(</span><span style="color:#e6db74">&#39;/fonts/BerkeleyMono.woff2&#39;</span><span style="color:#f92672">)</span> <span style="color:#f92672">format</span><span style="color:#f92672">(</span><span style="color:#e6db74">&#39;woff2&#39;</span><span style="color:#f92672">),</span> </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><div></div><div class="notice note"> <p class="first notice-title"><span class="icon-notice baseline"></span>WOFF2 vs WOFF(1)</p><p>A previous version of this post also included the <code>.woff</code> file in addition to <code>.woff2</code>. A kind reader let me know that <a href="https://caniuse.com/?search=woff2" rel="external">basically everything↗</a> supports <code>.woff2</code>, and since <code>.woff2</code> offers much better compression than first-generation <code>.woff</code> there <em>really</em> isn't any reason to offer a font in <code>.woff</code> format in this modern age. I can just offer <code>.woff2</code> on its own.</p> <p>I've updated this post, my CSS, and the contents of my CDN storage accordingly.</p></div> <p>And that would work just fine... but it <em>would</em> require storing those web font files in the (public) <a href="https://github.com/jbowdre/runtimeterror" rel="external">GitHub repo↗</a> which powers my site, and I'd rather not store any paid font files there.</p> <p>So instead, I opted to try using a <a href="https://en.wikipedia.org/wiki/Content_delivery_network" rel="external">Content Delivery Network (CDN)↗</a> to host the font files. This would allow for some degree of access control, help me learn more about a web technology I hadn't played with much, and make use of a cool <code>cdn.*</code> subdomain in the process.</p> <div class="notice note"> <p class="first notice-title"><span class="icon-notice baseline"></span>Double the CDN, double the fun</p><p>Of course, while writing this post I gave in to my impulsive nature and <a href="https://srsbsns.lol/post/i-just-hopped-to-bunny-net" rel="external">migrated the site from Cloudflare to Bunny.net↗</a>. Rather than scrap the content I'd already written, I'll go ahead and describe how I set this up first on <a href="https://www.cloudflare.com/developer-platform/r2/" rel="external">Cloudflare R2↗</a> and later on <a href="https://bunny.net/storage/" rel="external">Bunny Storage↗</a>.</p></div> <h4 id="cloudflare-r2"> Cloudflare R2 <a class="hlink" href="#cloudflare-r2"><i class="fa-solid fa-link"></i></a> </h4><p>Getting started with R2 was really easy; I just <a href="https://developers.cloudflare.com/r2/buckets/create-buckets/" rel="external">created a new R2 bucket↗</a> called <code>runtimeterror</code> and <a href="https://developers.cloudflare.com/r2/buckets/public-buckets/#connect-a-bucket-to-a-custom-domain" rel="external">connected it to the custom domain↗</a> <code>cdn.runtimeterror.dev</code>. I put the two web font files in a folder titled <code>fonts</code> and uploaded them to the bucket so that they can be accessed under <code>https://cdn.runtimeterror.dev/fonts/</code>.</p> <p>I could then employ a <a href="https://developers.cloudflare.com/r2/buckets/cors/" rel="external">Cross-Origin Resource Sharing (CORS)↗</a> policy to ensure the fonts hosted on my fledgling CDN can only be loaded on my site. I configured the policy to also allow access from my <code>localhost</code> Hugo build environment as well as a preview Neocities environment I use for testing such major changes:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>[ </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;AllowedOrigins&#34;</span>: [ </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;http://localhost:1313&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;https://secret--runtimeterror--preview.neocities.org&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;https://runtimeterror.dev&#34;</span> </span></span><span style="display:flex;"><span> ], </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;AllowedMethods&#34;</span>: [ </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;GET&#34;</span> </span></span><span style="display:flex;"><span> ] </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span>] </span></span></code></pre></div><p>Then I just needed to update the <code>@font-face</code> rule accordingly:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-css" data-lang="css"><span style="display:flex;"><span><span style="color:#75715e">/* load preferred font */</span> </span></span><span style="display:flex;"><span>@<span style="color:#66d9ef">font-face</span> { </span></span><span style="display:flex;"><span> <span style="color:#f92672">font-family</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Berkeley Mono&#39;</span><span style="color:#f92672">;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">font-style</span><span style="color:#f92672">:</span> <span style="color:#f92672">normal</span><span style="color:#f92672">;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">font-weight</span><span style="color:#f92672">:</span> <span style="color:#f92672">400</span><span style="color:#f92672">;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">font-display</span><span style="color:#f92672">:</span> <span style="color:#f92672">fallback</span><span style="color:#f92672">;</span> <span style="color:#75715e">/* [tl! ++] */</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">src</span><span style="color:#f92672">:</span> <span style="color:#f92672">local</span><span style="color:#f92672">(</span><span style="color:#e6db74">&#39;Berkeley Mono&#39;</span><span style="color:#f92672">),</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">url</span><span style="color:#f92672">(</span><span style="color:#e6db74">&#39;/fonts/BerkeleyMono.woff2&#39;</span><span style="color:#f92672">)</span> <span style="color:#f92672">format</span><span style="color:#f92672">(</span><span style="color:#e6db74">&#39;woff2&#39;</span><span style="color:#f92672">),</span> <span style="color:#75715e">/* [tl! --] */</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">url</span><span style="color:#f92672">(</span><span style="color:#e6db74">&#39;https://cdn.runtimeterror.dev/fonts/BerkeleyMono.woff2&#39;</span><span style="color:#f92672">)</span> <span style="color:#f92672">format</span><span style="color:#f92672">(</span><span style="color:#e6db74">&#39;woff2&#39;</span><span style="color:#f92672">),</span> <span style="color:#75715e">/* [tl! ++] */</span> </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>I added in the <code>font-display: fallback;</code> descriptor to address the fact that the site will now be loading a remote font. Rather than blocking and not rendering text until the preferred font is loaded, it will show text in one of the available fallback fonts. If the preferred font loads quickly enough, it will be swapped in; otherwise, it will just show up on the next page load. I figured this was a good middle-ground between wanting the site to load quickly while also looking the way I want it to.</p> <p>To test my work, I ran <code>hugo server</code> to build and serve the site locally on <code>http://localhost:1313</code>... and promptly encountered a cascade of CORS-related errors. I kept tweaking the policy and trying to learn more about what I'm doing (reminder: I'm bad at this), but just couldn't figure out what was preventing the font from being loaded.</p> <p>I <em>eventually</em> discovered that sometimes you need to clear Cloudflare's cache so that new policy changes will take immediate effect. Once I <a href="https://developers.cloudflare.com/cache/how-to/purge-cache/purge-everything/" rel="external">purged everything↗</a>, the errors went away and the font loaded successfully.</p> <h3 id="bunny-storage"> Bunny Storage <a class="hlink" href="#bunny-storage"><i class="fa-solid fa-link"></i></a> </h3><p>After migrating my domain to Bunny.net, the CDN font setup was pretty similar - but also different enough that it's worth mentioning. I started by creating a new Storage Zone named <code>runtimeterror-storage</code>, and selecting an appropriate-seeming set of replication regions. I then uploaded the same <code>fonts/</code> folder as before.</p> <p>To be able to access the files in Bunny Storage, I connected a new Pull Zone (called <code>runtimeterror-pull</code>) and linked that Pull Zone with the <code>cdn.runtimeterror.dev</code> hostname. I also made sure to enable the option to automatically generate a certificate for this host.</p> <p>Rather than needing me to understand CORS and craft a viable policy file, Bunny provides a clean UI with easy-to-understand options for configuring the pull zone security. I enabled the options to block root path access, block <code>POST</code> requests, block direct file access, and also added the same trusted referrers as before:</p> <p><img src="https://runtimeterror.dev/using-custom-font-hugo/bunny-cdn-security.png" alt="Bunny CDN security configuration"></p> <p>I made sure to use the same paths as I had on Cloudflare so I didn't need to update the Hugo config at all even after changing CDNs. That same CSS from before still works:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-css" data-lang="css"><span style="display:flex;"><span><span style="color:#75715e">/* load preferred font */</span> </span></span><span style="display:flex;"><span>@<span style="color:#66d9ef">font-face</span> { </span></span><span style="display:flex;"><span> <span style="color:#f92672">font-family</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Berkeley Mono&#39;</span><span style="color:#f92672">;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">font-style</span><span style="color:#f92672">:</span> <span style="color:#f92672">normal</span><span style="color:#f92672">;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">font-weight</span><span style="color:#f92672">:</span> <span style="color:#f92672">400</span><span style="color:#f92672">;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">font-display</span><span style="color:#f92672">:</span> <span style="color:#f92672">fallback</span><span style="color:#f92672">;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">src</span><span style="color:#f92672">:</span> <span style="color:#f92672">local</span><span style="color:#f92672">(</span><span style="color:#e6db74">&#39;Berkeley Mono&#39;</span><span style="color:#f92672">),</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">url</span><span style="color:#f92672">(</span><span style="color:#e6db74">&#39;https://cdn.runtimeterror.dev/fonts/BerkeleyMono.woff2&#39;</span><span style="color:#f92672">)</span> <span style="color:#f92672">format</span><span style="color:#f92672">(</span><span style="color:#e6db74">&#39;woff2&#39;</span><span style="color:#f92672">),</span> </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>I again tested locally with <code>hugo server</code> and confirmed that the font loaded from Bunny CDN without any CORS or other errors.</p> <p>So that's the web font for the web site sorted (twice); now let's tackle the font in the OpenGraph share images.</p> <h3 id="image-filter-text"> Image Filter Text <a class="hlink" href="#image-filter-text"><i class="fa-solid fa-link"></i></a> </h3><p>My <a href="https://runtimeterror.dev/dynamic-opengraph-images-with-hugo/">setup for generating the share images</a> leverages the Hugo <a href="https://gohugo.io/functions/images/text/" rel="external">images.Text↗</a> function to overlay text onto a background image, and it needs a TrueType font in order to work. I was previously just storing the required font directly in my GitHub repo so that it would be available during the site build, but I definitely don't want to do that with a paid TrueType font file. So I needed to come up with some way to provide the TTF file to the GitHub runner without making it publicly available.</p> <p>I recently figured out how I could <a href="https://runtimeterror.dev/gemini-capsule-gempost-github-actions/#publish-github-actions:~:text=name%3A%20Connect%20to%20Tailscale">use a GitHub Action to easily connect the runner to my Tailscale environment</a>, and I figured I could re-use that idea here - only instead of pushing something to my tailnet, I'll be pulling something out.</p> <h4 id="tailscale-setup"> Tailscale Setup <a class="hlink" href="#tailscale-setup"><i class="fa-solid fa-link"></i></a> </h4><p>So I SSH'd to the cloud server I'm already using for hosting my Gemini capsule, created a folder to hold the font file (<code>/opt/fonts/</code>), and copied the TTF file into there. And then I used <a href="https://runtimeterror.dev/tailscale-ssh-serve-funnel/#tailscale-serve">Tailscale Serve</a> to publish that folder internally to my tailnet:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>sudo tailscale serve --bg --set-path /fonts /opt/fonts/ <span style="color:#75715e"># [tl! .cmd]</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># [tl! .nocopy:4]</span> </span></span><span style="display:flex;"><span>Available within your tailnet: </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>https://node.tailnet-name.ts.net/fonts/ </span></span><span style="display:flex;"><span>|-- path /opt/fonts </span></span></code></pre></div><p>The <code>--bg</code> flag will run the share in the background and automatically start it with the system (like a daemon-mode setup).</p> <p>When I set up Tailscale for the Gemini capsule workflow, I configured the Tailscale ACL so that the GitHub runner (<code>tag:gh-bld</code>) could talk to my server (<code>tag:gh-srv</code>) over SSH:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-json" data-lang="json"><span style="display:flex;"><span> <span style="color:#e6db74">&#34;acls&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [ </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> <span style="color:#75715e">// github runner can talk to the deployment target </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">&#34;action&#34;</span>: <span style="color:#e6db74">&#34;accept&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;users&#34;</span>: [<span style="color:#e6db74">&#34;tag:gh-bld&#34;</span>], </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;ports&#34;</span>: [ </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;tag:gh-srv:22&#34;</span> </span></span><span style="display:flex;"><span> ], </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> ]<span style="color:#960050;background-color:#1e0010">,</span> </span></span></code></pre></div><p>I needed to update that ACL to allow communication over HTTPS as well:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-json" data-lang="json"><span style="display:flex;"><span> <span style="color:#e6db74">&#34;acls&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [ </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> <span style="color:#75715e">// github runner can talk to the deployment target </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">&#34;action&#34;</span>: <span style="color:#e6db74">&#34;accept&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;users&#34;</span>: [<span style="color:#e6db74">&#34;tag:gh-bld&#34;</span>], </span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;ports&#34;</span>: [ </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;tag:gh-srv:22&#34;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;tag:gh-srv:443&#34;</span> <span style="color:#75715e">// [tl! ++] </span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> ], </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> ]<span style="color:#960050;background-color:#1e0010">,</span> </span></span></code></pre></div><p>I then logged into the Tailscale admin panel to follow the same steps as last time to generate a unique <a href="https://tailscale.com/kb/1215/oauth-clients" rel="external">OAuth client↗</a> tied to the <code>tag:gh-bld</code> tag. I stored the ID, secret, and tags as repository secrets named <code>TS_API_CLIENT_ID</code>, <code>TS_API_CLIENT_SECRET</code>, and <code>TS_TAG</code>.</p> <p>I also created a <code>REMOTE_FONT_PATH</code> secret which will be used to tell Hugo where to find the required TTF file (<code>https://node.tailnet-name.ts.net/fonts/BerkeleyMono.ttf</code>).</p> <h4 id="hugo-setup"> Hugo Setup <a class="hlink" href="#hugo-setup"><i class="fa-solid fa-link"></i></a> </h4><p>Here's the image-related code that I was previously using in <code>layouts/partials/opengraph</code> to create the OpenGraph images:</p> <pre tabindex="0"><code class="language-jinja-html" data-lang="jinja-html">{{ $img := resources.Get &#34;og_base.png&#34; }} {{ $font := resources.Get &#34;/FiraMono-Regular.ttf&#34; }} {{ $text := &#34;&#34; }} {{- if .IsHome }} {{ $text = .Site.Params.Description }} {{- end }} {{- if .IsPage }} {{ $text = .Page.Title }} {{ end }} {{- with .Params.thumbnail }} {{ $thumbnail := $.Resources.Get . }} {{ with $thumbnail }} {{ $img = $img.Filter (images.Overlay (.Process &#34;fit 300x250&#34;) 875 38 )}} {{ end }} {{ end }} {{ $img = $img.Filter (images.Text $text (dict &#34;color&#34; &#34;#d8d8d8&#34; &#34;size&#34; 64 &#34;linespacing&#34; 2 &#34;x&#34; 40 &#34;y&#34; 300 &#34;font&#34; $font ))}} {{ $img = resources.Copy (path.Join $.Page.RelPermalink &#34;og.png&#34;) $img }} </code></pre><p>All I need to do is get it to pull the font resource from a web address rather than the local file system, and I'll do that by loading an environment variable instead of hardcoding the path here.</p> <div class="notice note"> <p class="first notice-title"><span class="icon-notice baseline"></span>Hugo Environent Variable Access</p><p>By default, Hugo's <code>os.Getenv</code> function only has access to environment variables which start with <code>HUGO_</code>. You can <a href="https://gohugo.io/functions/os/getenv/#security" rel="external">adjust the security configuration↗</a> to alter this restriction if needed, but I figured I could work just fine within the provided constraints.</p></div> <pre tabindex="0"><code class="language-jinja-html" data-lang="jinja-html">{{ $img := resources.Get &#34;og_base.png&#34; }} {{ $font := resources.Get &#34;/FiraMono-Regular.ttf&#34; }} &lt;!-- [tl! -- ] --&gt; {{ $text := &#34;&#34; }} {{ $font := &#34;&#34; }} &lt;!-- [tl! ++:10 **:10 ]&gt; {{ $path := os.Getenv &#34;HUGO_REMOTE_FONT_PATH&#34; }} {{ with resources.GetRemote $path }} {{ with .Err }} {{ errorf &#34;%s&#34; . }} {{ else }} {{ $font = . }} {{ end }} {{ else }} {{ errorf &#34;Unable to get resource %q&#34; $path }} {{ end }} {{- if .IsHome }} {{ $text = .Site.Params.Description }} {{- end }} &lt;!-- [tl! collapse:start ] --&gt; {{- if .IsPage }} {{ $text = .Page.Title }} {{ end }} {{- with .Params.thumbnail }} {{ $thumbnail := $.Resources.Get . }} {{ with $thumbnail }} {{ $img = $img.Filter (images.Overlay (.Process &#34;fit 300x250&#34;) 875 38 )}} {{ end }} {{ end }} {{ $img = $img.Filter (images.Text $text (dict &#34;color&#34; &#34;#d8d8d8&#34; &#34;size&#34; 64 &#34;linespacing&#34; 2 &#34;x&#34; 40 &#34;y&#34; 300 &#34;font&#34; $font ))}} {{ $img = resources.Copy (path.Join $.Page.RelPermalink &#34;og.png&#34;) $img }} &lt;!-- [tl! collapse:end ] --&gt; </code></pre><p>I can test that this works by running a build locally from a system with access to my tailnet. I'm not going to start a web server with this build; I'll just review the contents of the <code>public/</code> folder once it's complete to see if the OpenGraph images got rendered correctly.</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>HUGO_REMOTE_FONT_PATH<span style="color:#f92672">=</span>https://node.tailnet-name.ts.net/fonts/BerkeleyMono.ttf hugo </span></span></code></pre></div><p>Neat, it worked!</p> <p><img src="https://runtimeterror.dev/using-custom-font-hugo/og-sample.png" alt="OpenGraph share image for this post"></p> <h4 id="github-action"> GitHub Action <a class="hlink" href="#github-action"><i class="fa-solid fa-link"></i></a> </h4><p>All that's left is to update the GitHub Actions workflow I use for <a href="https://runtimeterror.dev/deploy-hugo-neocities-github-actions/">building and deploying my site to Neocities</a> to automate things:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># torchlight! {&#34;lineNumbers&#34;: true}</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># .github/workflows/deploy-to-neocities.yml</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy to Neocities</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># [tl! collapse:start]</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">schedule</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f92672">cron</span>: <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">13</span> * * * </span></span><span style="display:flex;"><span> <span style="color:#f92672">push</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">branches</span>: </span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">main</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#f92672">concurrency</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">group</span>: <span style="color:#ae81ff">deploy-to-neocities</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">cancel-in-progress</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#f92672">defaults</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">bash</span> </span></span><span style="display:flex;"><span><span style="color:#75715e"># [tl! collapse:end]</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">deploy</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build and deploy Hugo site</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">steps</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Hugo setup</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">peaceiris/actions-hugo@v2.6.0</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">with</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">hugo-version</span>: <span style="color:#e6db74">&#39;0.121.1&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">extended</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Checkout</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">with</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">submodules</span>: <span style="color:#ae81ff">recursive</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Connect to Tailscale</span> <span style="color:#75715e"># [tl! ++:10 **:10]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">tailscale/github-action@v2</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">with</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">oauth-client-id</span>: <span style="color:#ae81ff">${{ secrets.TS_API_CLIENT_ID }}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">oauth-secret</span>: <span style="color:#ae81ff">${{ secrets.TS_API_CLIENT_SECRET }}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">tags</span>: <span style="color:#ae81ff">${{ secrets.TS_TAG }}</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build with Hugo</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: <span style="color:#ae81ff">hugo --minify</span> <span style="color:#75715e"># [tl! -- **]</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: <span style="color:#ae81ff">HUGO_REMOTE_FONT_PATH=${{ secrets.REMOTE_FONT_PATH }} hugo --minify</span> <span style="color:#75715e"># [tl! ++ ** reindex(-1) ]</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Insert 404 page</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: |<span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> cp public/404/index.html public/not_found.html</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Highlight with Torchlight</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">run</span>: |<span style="color:#e6db74"> </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> npm i @torchlight-api/torchlight-cli </span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> npx torchlight</span> </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy to Neocities</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">bcomnes/deploy-to-neocities@v1</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">with</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">api_token</span>: <span style="color:#ae81ff">${{ secrets.NEOCITIES_API_TOKEN }}</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">cleanup</span>: <span style="color:#66d9ef">true</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">dist_dir</span>: <span style="color:#ae81ff">public</span> </span></span></code></pre></div><p>This uses the <a href="https://github.com/tailscale/github-action" rel="external">Tailscale GitHub Action↗</a> to connect the runner to my tailnet using the credentials I created earlier, and passes the <code>REMOTE_FONT_PATH</code> secret as an environment variable to the Hugo command line. Hugo will then be able to retrieve and use the TTF font during the build process.</p> <h3 id="conclusion"> Conclusion <a class="hlink" href="#conclusion"><i class="fa-solid fa-link"></i></a> </h3><p>Configuring and using a custom font in my Hugo-generated site wasn't hard to do, but I had to figure some things out on my own to get started in the right direction. I learned a lot about how fonts are managed in CSS along the way, and I love the way the new font looks on this site!</p> <p>This little project also gave me an excuse to play with first Cloudflare R2 and then Bunny Storage, and I came away seriously impressed by Bunny (and have since moved more of my domains to bunny.net). Expect me to write more about cool Bunny stuff in the future.</p> </description> </item> </channel> </rss>