<?xml version="1.0" encoding="utf-8" standalone="yes"?>

<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Thomas Letan's Blog - terraform</title>
    <link>https://soap.coffee/~lthms/tags/terraform.html</link>
    <description>Posts tagged "terraform"</description>
    <atom:link href="https://soap.coffee/~lthms/tags/terraform.xml" rel="self"
               type="application/rss+xml" />
    
    
    <item>
      <title>I Cannot SSH Into My Server Anymore (And That’s Fine)</title>
      <link>https://soap.coffee/~lthms/posts/i-cannot-ssh-into-my-server-anymore.html</link>
      <guid>https://soap.coffee/~lthms/posts/i-cannot-ssh-into-my-server-anymore.html</guid>
      <pubDate>January 5, 2026</pubDate>
      <description>
        
        &lt;h1&gt;I Cannot SSH Into My Server Anymore (And That’s Fine)&lt;/h1&gt;&lt;div id=&quot;tags-list&quot;&gt;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/coreos.html&quot; class=&quot;tag hover-coral&quot; marked=&quot;&quot;&gt;coreos&lt;/a&gt; &lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/docker.html&quot; class=&quot;tag hover-lavender&quot; marked=&quot;&quot;&gt;docker&lt;/a&gt; &lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/meta.html&quot; class=&quot;tag hover-mint&quot; marked=&quot;&quot;&gt;meta&lt;/a&gt; &lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/self-hosting.html&quot; class=&quot;tag hover-lavender&quot; marked=&quot;&quot;&gt;self-hosting&lt;/a&gt; &lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/terraform.html&quot; class=&quot;tag hover-lemon&quot; marked=&quot;&quot;&gt;terraform&lt;/a&gt; &lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/vultr.html&quot; class=&quot;tag hover-sky&quot; marked=&quot;&quot;&gt;vultr&lt;/a&gt; &lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;I would like to thank Yann Régis-Gianas, Sylvain Ribstein and Paul Laforgue
for their feedback and careful review.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To kick off 2026, I had clear objectives in mind: decommissioning &lt;code class=&quot;hljs&quot;&gt;moana&lt;/code&gt;, my
trusty $100+/month VPS, and setting up &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt;, its far less costly
successor.&lt;/p&gt;
&lt;p&gt;On the one hand, I have been using &lt;code class=&quot;hljs&quot;&gt;moana&lt;/code&gt; to self-host a number of services,
and it was very handy to know that I had always a go-to place to experiment
with whatever caught my interest. On the other hand, $100/month is obviously a
lot of money, and looking back at how I used it in 2025, it was not
particularly well spent. It was time to downsize.&lt;/p&gt;
&lt;p&gt;Now that &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; is up and running, I cannot even SSH into it. In fact,
&lt;em&gt;nothing&lt;/em&gt; can.&lt;/p&gt;
&lt;p&gt;There is no need. To update one of the services it hosts, I push a new container
image to the appropriate registry with the correct tag. &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; will fetch
and deploy it. All on its own.&lt;/p&gt;
&lt;p&gt;In this article, I walk through the journey that led me to the smoke and
mirrors behind this magic trick: &lt;a href=&quot;https://fedoraproject.org/coreos/&quot; class=&quot;hover-peach&quot; marked=&quot;&quot;&gt;Fedora CoreOS&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;, &lt;a href=&quot;https://coreos.github.io/ignition/&quot; class=&quot;hover-mint&quot; marked=&quot;&quot;&gt;Ignition&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; and &lt;a href=&quot;https://docs.podman.io/en/latest/markdown/podman-quadlet.1.html&quot; class=&quot;hover-sky&quot; marked=&quot;&quot;&gt;Podman
Quadlets&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; in the main roles, with &lt;a href=&quot;https://developer.hashicorp.com/terraform&quot; class=&quot;hover-coral&quot; marked=&quot;&quot;&gt;Terraform&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; as an essential supporting
character. This stack checks all the boxes I care about.&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-note&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-info mr-2&quot; viewbox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Note&lt;/p&gt;&lt;p&gt;For interested readers, I have published &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt;’s &lt;a href=&quot;https://github.com/lthms/tinkerbell&quot; class=&quot;hover-lavender&quot; marked=&quot;&quot;&gt;full setup&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; on
GitHub. This article reads as an experiment log, and if you are only
interested in the final result, you should definitely have a look.&lt;/p&gt;
&lt;/div&gt;
&lt;h2&gt;Container-Centric, Declarative, and Low-Maintenance&lt;/h2&gt;
&lt;p&gt;Going into this, I knew I didn’t want to reproduce &lt;code class=&quot;hljs&quot;&gt;moana&lt;/code&gt;’s setup—it was fully
manual&lt;label for=&quot;fn1&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn1&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;In the end, I never took the time to publish a write-up about it, so
in a nutshell: everything relied on &lt;a href=&quot;https://github.com/lthms/nspawn&quot; class=&quot;hover-periwinkle&quot; marked=&quot;&quot;&gt;a small script&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; that enabled
me to create interconnected &lt;a href=&quot;https://man7.org/linux/man-pages/man5/systemd.nspawn.5.html&quot; class=&quot;hover-coral&quot; marked=&quot;&quot;&gt;nspawn containers&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; on the spot. &lt;/span&gt;
&lt;/span&gt; and I no longer have the time or the motivation to fiddle with
the internals of a server. Instead, I wanted to embrace the principles my
DevOps colleagues had taught me over the past two years.&lt;/p&gt;
&lt;p&gt;My initial idea was to start with this very website, since it was the only
service deployed on &lt;code class=&quot;hljs&quot;&gt;moana&lt;/code&gt; that I really wanted to keep. Since &lt;a href=&quot;./DreamWebsite.html&quot; class=&quot;hover-lavender&quot; marked=&quot;&quot;&gt;I had written
a container image for this website&lt;/a&gt;, I just had to look
for the most straightforward and future-proof way to deploy it in production™—
something I could later extend to deploy more cool projects, if I ever wanted
to&lt;label for=&quot;fn2&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn2&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;If my goal was limited to host a static website, this whole setup
would arguably be both overengineered and counterproductive. &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt;
was to become my little foothold in the Internet, though. My “cloud
homelabs,” of sorts. &lt;/span&gt;
&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/compose/&quot; class=&quot;hover-peach&quot; marked=&quot;&quot;&gt;Docker Compose&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; alone wasn’t a good fit. I like compose files, but one needs
to provision and manage a VM to host them. Ansible can provision VMs, but that
road comes with its own set of struggles. Writing good playbooks has always
felt surprisingly difficult to me. In particular, a good playbook is supposed
to handle two very different scenarios—provisioning a brand new machine, and
updating a pre-existing deployment—and I have found it particularly challenging
to ensure that both paths reliably produce the same result.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://kubernetes.io/&quot; class=&quot;hover-lavender&quot; marked=&quot;&quot;&gt;Kubernetes&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; was &lt;em&gt;very&lt;/em&gt; appealing on paper. I have seen engineers turn compose
files into &lt;a href=&quot;https://helm.sh/docs/topics/charts/&quot; class=&quot;hover-rose&quot; marked=&quot;&quot;&gt;Helm charts&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; and be done with it. If I could do the same thing,
wouldn’t that be bliss? Unfortunately, Kubernetes is a notoriously complex
stack, resulting from compromises made to address challenges I simply don’t
face. Managed clusters could make things easier, but they aren’t cheap. That
would defeat the initial motivation behind retiring &lt;code class=&quot;hljs&quot;&gt;moana&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://fedoraproject.org/coreos/&quot; class=&quot;hover-peach&quot; marked=&quot;&quot;&gt;CoreOS&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;, being an operating system specifically built to &lt;em&gt;run containers&lt;/em&gt;,
obviously stood out. That said, I had very little intuition on how it could
work in practice. So I started digging. I learned about Ignition first. Its
purpose is to provision a VM exactly once, at first boot. If you need to change
something afterwards, you throw away your VM and create a new one. This may
seem counter-intuitive, but since it eliminates the main reason I was looking
for an alternative to Ansible, I was hooked&lt;label for=&quot;fn3&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn3&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;CoreOS and Ignition enable me to think about virtual machines the same
way OCaml or Haskell trained me to think about data: as immutable values. &lt;/span&gt;
&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;I found out how to use systemd unit files to start containers via &lt;code class=&quot;hljs&quot;&gt;podman&lt;/code&gt; CLI
commands. That was way too cumbersome, so I pushed on for a way to orchestrate
containers &lt;em&gt;à la&lt;/em&gt; Docker Compose. That’s when I discovered Podman Quadlets and
&lt;a href=&quot;https://docs.podman.io/en/stable/markdown/podman-auto-update.1.html&quot; class=&quot;hover-rose&quot; marked=&quot;&quot;&gt;auto-updates&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With that, everything clicked. I knew what I wanted to do, and I was very
excited about it.&lt;/p&gt;
&lt;h2&gt;Assembling &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;For more than a year now, my website has been &lt;a href=&quot;./DreamWebsite.html&quot; class=&quot;hover-lavender&quot; marked=&quot;&quot;&gt;served from RAM by a standalone,
static binary built in OCaml&lt;/a&gt;, with TLS termination handled by Nginx and
&lt;code class=&quot;hljs&quot;&gt;certbot&lt;/code&gt;’s certificates renewal performed by yours truly&lt;label for=&quot;fn4&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn4&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;I didn’t lie when I said &lt;code class=&quot;hljs&quot;&gt;moana&lt;/code&gt;’s setup was indeed &lt;em&gt;manual&lt;/em&gt;. &lt;/span&gt;
&lt;/span&gt;. I didn’t
have any reason to fundamentally change this architecture. I was simply looking
for a way to automate their deployment.&lt;/p&gt;
&lt;h3&gt;Container-Centric, …&lt;/h3&gt;
&lt;p&gt;The logical thing to do was to have &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; run two containers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The reverse proxy:&lt;/strong&gt; I had been firmly on Team Nginx for years now, but
when I heard &lt;a href=&quot;https://caddyserver.com/&quot; class=&quot;hover-coral&quot; marked=&quot;&quot;&gt;Caddy&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; could “&lt;em&gt;automatically obtain&lt;/em&gt; and &lt;em&gt;renew&lt;/em&gt; TLS
certificates,” I was sold on giving it a try.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The website itself:&lt;/strong&gt; Static binaries can be wrapped inside a container
with close to zero overhead using the &lt;a href=&quot;https://hub.docker.com/_/scratch&quot; class=&quot;hover-lemon&quot; marked=&quot;&quot;&gt;&lt;code class=&quot;hljs&quot;&gt;scratch&lt;/code&gt;&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; base image, so I
did just that. I published it to a free-plan, public registry hosted on Vultr
that I created for the occasion&lt;label for=&quot;fn5&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn5&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;Which means getting an offline copy of this website is now as
simple as calling &lt;code class=&quot;hljs&quot;&gt;docker pull ams.vultrcr.com/lthms/www/soap.coffee:live&lt;/code&gt;. &lt;/span&gt;
&lt;/span&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;https://mermaid.ink/img/pako:eNo9UMtOwzAQ_BVrTyCllZs4JPEBiYZjuQASEnUPTrxJLBK7ch1BafPvuC_2tDM7M7vaA9RWIXBoevtdd9J5snoVhoR6Wpe9RuM3ZDZ7PPKcEs5YciTLtYBSKrUnd--rt3sBm4t-N1atk9uOeG2-0FXY95fB8hZwJOX6A6ud9nj1oFEQQeu0Au7diBEM6AZ5gnA4SQT4DgcUwENrcPRO9gKEmYJtK82ntcPN6ezYdsAb2e8CGrdKenzWMtw0_LMuLERX2tF44AnLziHAD_ATIM3mMY3zlCZskRUPRQR74DGL58WCXrk8S6cIfs9b6TxlWZ4EcsFYkdOgR6W9dS-Xj9bWNLqF6Q97DWuS?type=png&quot;&gt;&lt;figcaption&gt;&lt;p&gt;Nothing beats a straightforward architecture&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Nothing fancy or unexpected here, which made it a good target for a first
deployment. It was time to open Neovim to write some YAML.&lt;/p&gt;
&lt;h3&gt;Declarative, …&lt;/h3&gt;
&lt;p&gt;At this point, the architecture was clear. The next step was to turn it into
something a machine could execute. To that end, I needed two things: first an
Ignition configuration, then a CoreOS VM to run it.&lt;/p&gt;
&lt;h4&gt;The Proof of Concept&lt;/h4&gt;
&lt;p&gt;Ignition configurations (&lt;code class=&quot;hljs&quot;&gt;.ign&lt;/code&gt;) are JSON files primarily intended to be
consumed by machines. They are produced from YAML files using a tool called
&lt;a href=&quot;https://coreos.github.io/butane/&quot; class=&quot;hover-sky&quot; marked=&quot;&quot;&gt;Butane&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;. For instance, here is the first Butane configuration file I ended up
writing. It provisions a CoreOS VM by creating a new user (&lt;code class=&quot;hljs&quot;&gt;lthms&lt;/code&gt;), along with
a &lt;code class=&quot;hljs&quot;&gt;.ssh/authorized_keys&lt;/code&gt; file allowing me to SSH into the VM&lt;label for=&quot;fn6&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn6&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;I didn’t know at the time that I would &lt;em&gt;deliberately&lt;/em&gt; remove these
lines from the final Butane file. &lt;/span&gt;
&lt;/span&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;variant:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;fcos&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;version:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1.5&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;.0&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;passwd:&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;users:&lt;/span&gt;
    &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;lthms&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;ssh_authorized_keys:&lt;/span&gt;
        &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;ssh-ed25519&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;AAAAC3NzaC1lZDI1NTE5AAAAIKajIx3VWRjhqIrza4ZnVnnI1g2q6NfMfMOcnSciP1Ws&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;lthms@vanellope&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What’s important to keep in mind is that Ignition runs exactly once, at first
boot. Then it is never used again. This single fact has far-reaching
consequences, and is the reason why any meaningful change implies replacing the
machine, not modifying it.&lt;/p&gt;
&lt;p&gt;Before going any further, I wanted to understand how the actual deployment was
going to work. I generated the Ignition configuration file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;butane main.bu &amp;gt; main.ign
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, I decided to investigate how to define the Vultr VM in Terraform. The
resulting configuration is twofold. First, we need to configure Terraform to be
able to interact with the Vultr API, using the &lt;a href=&quot;https://registry.terraform.io/providers/vultr/vultr/latest/docs&quot; class=&quot;hover-periwinkle&quot; marked=&quot;&quot;&gt;Vultr provider&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;. Second, I
needed to &lt;a href=&quot;https://registry.terraform.io/providers/vultr/vultr/latest/docs/resources/instance&quot; class=&quot;hover-peach&quot; marked=&quot;&quot;&gt;create the VM&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;&lt;label for=&quot;fn7&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn7&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;For discovering what values to put in most fields, &lt;code class=&quot;hljs&quot;&gt;vultr-cli&lt;/code&gt; is
pretty convenient. Kudos to the Vultr team for making it in the first
place. &lt;/span&gt;
&lt;/span&gt; and feed it the Ignition
configuration.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-hcl&quot;&gt;resource &quot;vultr_instance&quot; &quot;tinkerbell&quot; {
  region = &quot;cdg&quot;
  plan = &quot;vc2-1c-1gb&quot;
  os_id = &quot;391&quot;

  label = &quot;tinkerbell&quot;
  hostname = &quot;tinkerbell&quot;

  user_data = file(&quot;main.ign&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that was it. I invoked &lt;code class=&quot;hljs&quot;&gt;terraform apply&lt;/code&gt;, waited for a little while, then
SSHed into the newly created VM with my &lt;code class=&quot;hljs&quot;&gt;lthms&lt;/code&gt; user. Sure enough, the
&lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; VM was now listed in the Vultr web interface. I explored for a
little while, then called &lt;code class=&quot;hljs&quot;&gt;terraform destroy&lt;/code&gt; and rejoiced when everything
worked as expected.&lt;/p&gt;
&lt;h4&gt;The MVP&lt;/h4&gt;
&lt;p&gt;At this point, I was basically done with Terraform, and I just needed to write
the Butane configuration that would bring my containers to life. As I mentioned
earlier, the first approach I tried was to define a systemd service responsible
for invoking &lt;code class=&quot;hljs&quot;&gt;podman&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;systemd:&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;units:&lt;/span&gt;
    &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;soap.coffee.service&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;enabled:&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;contents:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;|
        [Unit]
        Description=Web Service
        After=network-online.target
        Wants=network-online.target
&lt;/span&gt;
        [&lt;span class=&quot;hljs-string&quot;&gt;Service&lt;/span&gt;]
        &lt;span class=&quot;hljs-string&quot;&gt;ExecStart=/usr/bin/podman&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;\&lt;/span&gt;
          &lt;span class=&quot;hljs-string&quot;&gt;--name&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;soap.coffee&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;\&lt;/span&gt;
          &lt;span class=&quot;hljs-string&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;8901&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:8901&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;\&lt;/span&gt;
          &lt;span class=&quot;hljs-string&quot;&gt;--restart=always&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;\&lt;/span&gt;
          &lt;span class=&quot;hljs-string&quot;&gt;ams.vultrcr.com/lthms/www/soap.coffee:latest&lt;/span&gt;
        &lt;span class=&quot;hljs-string&quot;&gt;ExecStop=/usr/bin/podman&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;stop&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;soap.coffee&lt;/span&gt;

        [&lt;span class=&quot;hljs-string&quot;&gt;Install&lt;/span&gt;]
        &lt;span class=&quot;hljs-string&quot;&gt;WantedBy=multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding this entry in my Butane configuration and redeploying &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; got
me exactly what I wanted. My website was up and running. For the sake of
getting something working first, I added the necessary configuration for Caddy
(the container and the provisioning of its configuration file), redeployed
&lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; again, only to realize I also needed to create a network so that
the two containers could talk together. After half an hour or so, I got
everything working, but was left with a sour taste in my mouth.&lt;/p&gt;
&lt;p&gt;This would simply not do. I wasn’t defining anything, I was writing a shell
script in the most cumbersome way possible.&lt;/p&gt;
&lt;p&gt;Then, I remembered my initial train of thought and started to search for a way
to have Docker Compose work on CoreOS&lt;label for=&quot;fn8&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn8&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;Well, Podman Compose, I guess. &lt;/span&gt;
&lt;/span&gt;. That is when I discovered
Quadlet, whose &lt;a href=&quot;https://github.com/containers/quadlet&quot; class=&quot;hover-mint&quot; marked=&quot;&quot;&gt;initial repository does a good job justifying its
existence&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;&lt;label for=&quot;fn9&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn9&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;This repository is now archived, since Quadlet has got merged
upstream. &lt;/span&gt;
&lt;/span&gt;. In particular,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;With quadlet, you describe how to run a container in a format that is very
similar to regular systemd config files. From these actual systemd
configurations are automatically generated (using &lt;a href=&quot;https://github.com/containers/quadlet&quot; class=&quot;hover-peach&quot; marked=&quot;&quot;&gt;systemd generators&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To give a concrete example, here is the &lt;code class=&quot;hljs&quot;&gt;.container&lt;/code&gt; file I wrote for my
website server.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ini&quot;&gt;&lt;span class=&quot;hljs-section&quot;&gt;[Container]&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;ContainerName&lt;/span&gt;=soap.c&lt;span class=&quot;hljs-literal&quot;&gt;off&lt;/span&gt;ee
&lt;span class=&quot;hljs-attr&quot;&gt;Image&lt;/span&gt;=ams.vultrcr.com/lthms/www/soap.c&lt;span class=&quot;hljs-literal&quot;&gt;off&lt;/span&gt;ee:live

&lt;span class=&quot;hljs-section&quot;&gt;[Service]&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;Restart&lt;/span&gt;=always

&lt;span class=&quot;hljs-section&quot;&gt;[Install]&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;WantedBy&lt;/span&gt;=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I wasn’t wasting my time teaching systemd how to start containers anymore. I
was now declaring what should exist, so that systemd—repurposed for the
occasion as a container orchestrator—could take care of the rest.&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-tip&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-light-bulb mr-2&quot; viewbox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Tip&lt;/p&gt;&lt;p&gt;If your containers are basically ignored by systemd, be smarter than me. Do
not try to blindly change your &lt;code class=&quot;hljs&quot;&gt;.container&lt;/code&gt; files and redeploy your VM in a
very painful and frustrating loop. Simply ask systemd for the generator logs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs&quot;&gt;sudo journalctl -b | grep -i quadlet 
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;I excitedly turned &lt;code class=&quot;hljs&quot;&gt;caddy.service&lt;/code&gt; into &lt;code class=&quot;hljs&quot;&gt;caddy.container&lt;/code&gt;, redeployed
&lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt;, ran into the exact same issue I had encountered before and
discovered the easiest way for two Quadlet-defined containers to talk to each
other was to introduce a &lt;a href=&quot;https://docs.podman.io/en/latest/_static/api.html?version=latest#tag/pods&quot; class=&quot;hover-lemon&quot; marked=&quot;&quot;&gt;&lt;em&gt;pod&lt;/em&gt;&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;. Unlike Docker Compose which uses DNS
over a bridge network, a pod shares the network namespace, allowing containers
to communicate over &lt;em&gt;localhost&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;To define a pod, one needs to create a &lt;code class=&quot;hljs&quot;&gt;.pod&lt;/code&gt; file, and to reference it in
their &lt;code class=&quot;hljs&quot;&gt;.container&lt;/code&gt; files using the &lt;code class=&quot;hljs&quot;&gt;PodName=&lt;/code&gt; configuration option. A “few”
redeployments later, I got everything working again, and I was ready to call it
a day.&lt;/p&gt;
&lt;p&gt;And with that, &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; was basically ready.&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-caution&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-stop mr-2&quot; viewbox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Caution&lt;/p&gt;&lt;p&gt;I’ve later learned that restarting a container that is part of a pod will
have the (to me, unexpected) side-effect to restart all the other containers
of that pod.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;And Low-Maintenance&lt;/h3&gt;
&lt;p&gt;Now, the end of the previous section might have given you pause.&lt;/p&gt;
&lt;p&gt;Even a static website like this one isn’t completely “stateless.” Not only does
Caddy require a configuration file to do anything meaningful, but it is also a
stateful application as it manages TLS certificates over time. Besides, I &lt;em&gt;do&lt;/em&gt;
publish technical write-ups from time to time&lt;label for=&quot;fn10&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn10&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;Two in 2025, that’s true. But 2026 is only starting, you never know
what might come! &lt;/span&gt;
&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Was I really at peace with having to destroy and redeploy &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; every
time I need to change anything on my website?&lt;/p&gt;
&lt;p&gt;On the one hand, &lt;em&gt;yes&lt;/em&gt;. I believe I could live with that. I modify my website
only a handful of times even in good months, I think my audience could survive
with a minute of downtime before being allowed to read my latest pieces. It may
be an unpopular opinion, but considering my actual use case, it &lt;em&gt;was&lt;/em&gt; good
enough. Even the fact that I do not store the TLS certificates obtained by
Caddy anywhere persistent should not be an issue. I mean, Let’s Encrypt has
fairly generous weekly issuance limits per domain &lt;label for=&quot;fn11&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn11&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;How do I know that? Well... I might have hit the limit while hacking my
way to a working setup. &lt;/span&gt;
&lt;/span&gt;. I should be fine.&lt;/p&gt;
&lt;p&gt;On the other hand, the setup was starting to grow on me, and I have &lt;em&gt;other&lt;/em&gt; use
cases in mind that could be a good fit for it. So I started researching again,
this time to understand how a deployment philosophy so focused on immutability
was managing what seemed to be conflicting requirements.&lt;/p&gt;
&lt;p&gt;I went down other rabbit holes, looking for answers. The discovery that stood
out the most to me—to the point where it became the hook of this article—was
Podman auto-updates.&lt;/p&gt;
&lt;p&gt;To deploy a new version of a containerized application, you pull the new image
and restart the container. When you commit to this pattern, why should you be
the one performing this action? Instead, your VM can regularly check registries
for new images, and &lt;em&gt;update the required containers when necessary&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;In practice, Podman made this approach trivial to put in place. I just needed
to label my containers with &lt;code class=&quot;hljs&quot;&gt;io.containers.autoupdate&lt;/code&gt; set to &lt;code class=&quot;hljs&quot;&gt;registry&lt;/code&gt;,
enable the &lt;code class=&quot;hljs&quot;&gt;podman-auto-update&lt;/code&gt; timer&lt;label for=&quot;fn12&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn12&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;By default, the timer is triggered once a day, which felt
unnecessarily long, so I decided to make it hourly instead. &lt;/span&gt;
&lt;/span&gt;, and that was it. Now, every
time I update the tag &lt;code class=&quot;hljs&quot;&gt;www/soap.coffee:live&lt;/code&gt; to point to a newer version of my
image, my website is updated within the hour.&lt;/p&gt;
&lt;p&gt;And that is when the final piece clicked. At this point, publishing an image
becomes the only deployment step. I didn’t need SSH anymore.&lt;/p&gt;
&lt;h2&gt;The Road Ahead&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; has been running for a few days now, and I am quite pleased with
the system I have put in place. In retrospect, none of this is particularly
novel. It feels more like I am converging toward a set of practices the
industry has been gravitating toward for years.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;/~lthms/img/iac-meme.jpg&quot;&gt;&lt;figcaption&gt;&lt;p&gt;A man looking at the “CoreOS &amp;amp; Quadlets” butterfly and wondering whether he’s looking at Infrastructure as Code. I’m not entirely sure of the answer.&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The journey is far from being over, though. &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; is up and running, and
it served you this HTML page just fine, but the moment I put SSH out of the
picture, it became a black box. Aside from some hardware metrics kindly
provided by the Vultr dashboard, I have no real visibility into what’s going on
inside. That is fine for now, but it is not a place I want to stay in forever.
I plan to spend a few more weekends building an observability stack&lt;label for=&quot;fn13&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn13&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;Oh, and maybe I will move these TLS certificates in a block storage or
something. That could be a good idea. &lt;/span&gt;
&lt;/span&gt;.
That will come in handy when things go wrong—as they inevitably do. I would
rather have the means to understand failures than guess my way around them.&lt;/p&gt;
&lt;p&gt;Did I ever mention I am an enthusiastic Opentelemetry convert?&lt;/p&gt;
        
      </description>
    </item>
    
    
  </channel>
</rss>
