<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: BrettOps</title>
    <description>The latest articles on DEV Community by BrettOps (@brettops).</description>
    <link>https://dev.to/brettops</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F7003%2F0f9feebe-b906-4b42-b1ea-a43b82018c87.png</url>
      <title>DEV Community: BrettOps</title>
      <link>https://dev.to/brettops</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brettops"/>
    <language>en</language>
    <item>
      <title>I was on the Bright Founders Talk podcast!</title>
      <dc:creator>Brett Weir</dc:creator>
      <pubDate>Mon, 17 Jul 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/brettops/i-was-on-the-bright-founders-talk-podcast-3743</link>
      <guid>https://dev.to/brettops/i-was-on-the-bright-founders-talk-podcast-3743</guid>
      <description>&lt;p&gt;This week is a BrettOps first. I was honored to be a guest on &lt;a href="https://www.temy.co/" rel="noopener noreferrer"&gt;Temy's&lt;/a&gt; &lt;a href="https://www.youtube.com/@temyco" rel="noopener noreferrer"&gt;Bright Founders Talk&lt;/a&gt; podcast, where I got to talk about my philosophy on infrastructure, work-life balance, and entrepreneurship.&lt;/p&gt;

&lt;p&gt;As an add-on achievement, I actually found an opportunity to work in a prodigious Python packaging alliteration right in the middle of the interview. 😆&lt;/p&gt;

&lt;p&gt;I want to say thank you to Temy for having me on, and to Matthew Wickham for being an excellent host.&lt;/p&gt;

&lt;p&gt;Enjoy!&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/eTyRfd4a1PA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>entrepreneurship</category>
      <category>infrastructure</category>
      <category>podcast</category>
      <category>update</category>
    </item>
    <item>
      <title>Meet cici-tools, a multi-tool for building GitLab CI/CD pipelines</title>
      <dc:creator>Brett Weir</dc:creator>
      <pubDate>Mon, 03 Jul 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/brettops/meet-cici-tools-a-multi-tool-for-building-gitlab-cicd-pipelines-7nf</link>
      <guid>https://dev.to/brettops/meet-cici-tools-a-multi-tool-for-building-gitlab-cicd-pipelines-7nf</guid>
      <description>&lt;p&gt;I've been working on a new project called &lt;a href="https://gitlab.com/brettops/tools/cici-tools" rel="noopener noreferrer"&gt;&lt;code&gt;cici-tools&lt;/code&gt;&lt;/a&gt; (pronounced "see-see"). It provides a set of command line tools for working with GitLab CI/CD files, where each tool does something useful in its own right. The direction of the project has changed quite a bit in trying to understand what is most needed and what can be reasonably built, but it's gotten to a good enough place to start talking about it.&lt;/p&gt;

&lt;p&gt;This project is still &lt;strong&gt;experimental&lt;/strong&gt; and the documentation is a work in progress. I can't promise that it works very well at the moment and would forgive you for not wanting to try it, but for the enterprising among you, I would love your feedback and to know if you found it useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cici-tools&lt;/code&gt; is available on &lt;a href="https://pypi.org/project/cici-tools/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;, so you can install it with &lt;code&gt;pip&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;cici-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will install the &lt;code&gt;cici&lt;/code&gt; command into your local environment, which you can validate like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cici &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cici &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;cici 0.2.5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Format CI files with &lt;code&gt;cici fmt&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;cici fmt&lt;/code&gt; tool mostly happened by accident while developing the &lt;code&gt;cici&lt;/code&gt; tool. While it hasn't always been clear what I am building, it was always clear that it would modify GitLab CI files in some way.&lt;/p&gt;

&lt;p&gt;In my early efforts, I wrote the tool to make as few changes as possible. Then I thought to create a new CI format that compiles back to GitLab CI. In the most recent iteration, &lt;code&gt;cici&lt;/code&gt; now implements &lt;a href="https://gitlab.com/brettops/tools/cici-tools/-/blob/main/cici/providers/gitlab/models.py" rel="noopener noreferrer"&gt;GitLab CI's schema directly in Python&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This latest approach has been the most time-consuming so far, but it has meant that reading a file in and writing it back out corrects the formatting in the process. Hence, &lt;code&gt;cici fmt&lt;/code&gt; was born.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cici fmt&lt;/code&gt; can be run with or without files as parameters, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cici &lt;span class="nb"&gt;fmt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cici &lt;span class="nb"&gt;fmt&lt;/span&gt;
&lt;span class="go"&gt;.gitlab-ci.yml formatted
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no file is passed, it defaults to a file in the current directory named &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;cici fmt&lt;/code&gt; is run, it will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Add quotes to strings where the syntax would be ambiguous otherwise,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reorder jobs in the file,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix indents and line spacing,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And some other random stuff.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Currently, it will also expand YAML anchors and &lt;code&gt;extends&lt;/code&gt; keywords, because it shares a certain code path with &lt;code&gt;cici bundle&lt;/code&gt;, even though it shouldn't. So it's not quite ready for prime time, but I'm working on it.&lt;/p&gt;

&lt;p&gt;Once it's finished, it'll be pretty exciting to have a GitLab CI linter that doesn't require calling out to a GitLab instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin &lt;code&gt;include&lt;/code&gt; versions with &lt;code&gt;cici update&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;There's a question I've pondered for a long time. How does one create shared pipelines, push updates to everyone quickly, and also track changes over time?&lt;/p&gt;

&lt;p&gt;There are two obvious choices, with their own obvious problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If everyone uses &lt;code&gt;main&lt;/code&gt; / &lt;code&gt;latest&lt;/code&gt; / what have you, everyone picks up changes immediately, and no one has any idea what versions are in use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If everyone pins to a specific version, they know exactly what versions are in use and will likely never upgrade them unless they absolutely have to.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;cici update&lt;/code&gt; offers a third choice: developers can continuously track the latest pipeline changes using a version-pinning tool so that they always know what versions they have, but are also able to pick up updates automatically.&lt;/p&gt;

&lt;p&gt;Here's an example CI file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .gitlab-ci.yml&lt;/span&gt;
&lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;brettops/pipelines/prettier&lt;/span&gt;
    &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;include.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;brettops/pipelines/python&lt;/span&gt;
    &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;lint.yml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;setuptools.yml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;twine.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now call &lt;code&gt;cici update&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cici update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cici update
&lt;span class="go"&gt;brettops/pipelines/prettier pinned to 0.1.0
brettops/pipelines/python is the latest at 0.5.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you check back into that CI file, you'll see pinned versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .gitlab-ci.yml&lt;/span&gt;
&lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;brettops/pipelines/prettier&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.1.0&lt;/span&gt;
    &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;include.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;brettops/pipelines/python&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.5.0&lt;/span&gt;
    &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;lint.yml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;setuptools.yml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;twine.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! That's all it does, but it helps me a lot.&lt;/p&gt;

&lt;p&gt;Add the pre-commit hook to your project and &lt;code&gt;cici update&lt;/code&gt; will run on every commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .pre-commit-config.yaml&lt;/span&gt;
&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# other hooks ...&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://gitlab.com/brettops/tools/cici-tools&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.2.5"&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;update&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cici update&lt;/code&gt; pulls the latest &lt;a href="https://docs.gitlab.com/ee/user/project/releases/" rel="noopener noreferrer"&gt;GitLab Release&lt;/a&gt; for a pipeline project by date, so there are no versioning requirements for the upstream, but it does mean that the upstream needs to publish regular releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundle CI files with &lt;code&gt;cici bundle&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;cici bundle&lt;/code&gt; command splits a large CI file into many CI "bundles", one for each job. These bundled CI files have everything each job needs to run, so that each job can be consumed à la carte by downstream projects. It currently expands &lt;code&gt;extends&lt;/code&gt; keywords, YAML anchors, and global variable declarations, with &lt;code&gt;include&lt;/code&gt; expansion planned.&lt;/p&gt;

&lt;p&gt;Let's use the &lt;a href="https://gitlab.com/brettops/pipelines/python" rel="noopener noreferrer"&gt;&lt;code&gt;brettops/pipelines/python&lt;/code&gt; pipeline&lt;/a&gt; as an example. It provides a large number of jobs that all depend on one or two base jobs. I won't attempt to reproduce its &lt;a href="https://gitlab.com/brettops/pipelines/python/-/blob/main/.cici/.gitlab-ci.yml" rel="noopener noreferrer"&gt;growing CI file&lt;/a&gt;, but here are a few jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .gitlab-ci.yml&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="na"&gt;python-black&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.python-base-small&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$PYTHON -m pip install black&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$PYTHON -m black --check --diff .&lt;/span&gt;

&lt;span class="na"&gt;python-isort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.python-base-small&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$PYTHON -m pip install isort&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$PYTHON -m isort --profile=black --check --diff .&lt;/span&gt;

&lt;span class="na"&gt;python-mypy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.python-base&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;*python-script-pip-install&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$PYTHON -m pip install mypy&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$PYTHON -m mypy "${PYTHON_PACKAGE}" --junit-xml report.xml&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;reports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;junit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;report.xml&lt;/span&gt;

&lt;span class="na"&gt;python-pyroma&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.python-base-small&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$PYTHON -m pip install pyroma&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$PYTHON -m pyroma -n "$PYTHON_PYROMA_MINIMUM_RATING" .&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;setup.py&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd like to use only some of the jobs here, but not all of them, you've got yourself a pickle. Splitting it into multiple CI files means that they'll need to depend on another file containing &lt;code&gt;.python-base&lt;/code&gt; or &lt;code&gt;.python-base-small&lt;/code&gt;. If you try to include more than one of these split files, GitLab will refuse, citing diamond inheritance. Ouch!&lt;/p&gt;

&lt;p&gt;To overcome this, &lt;code&gt;cici bundle&lt;/code&gt; will act as a compiler and build final versions that are independent from one another. Here's a bundled version of the &lt;code&gt;python-pyroma&lt;/code&gt; job from above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pyroma.yml&lt;/span&gt;
&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;

&lt;span class="na"&gt;workflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_PIPELINE_SOURCE == "push" &amp;amp;&amp;amp; $CI_OPEN_MERGE_REQUESTS&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

&lt;span class="na"&gt;python-pyroma&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${CONTAINER_PROXY}python:${PYTHON_VERSION}-alpine"&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;GIT_DEPTH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
    &lt;span class="na"&gt;GIT_SUBMODULE_STRATEGY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none"&lt;/span&gt;
    &lt;span class="na"&gt;PIP_CONFIG_FILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$PYTHON_PIP_CONFIG_FILE"&lt;/span&gt;
    &lt;span class="na"&gt;PYTHON&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/python3"&lt;/span&gt;
  &lt;span class="na"&gt;before_script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;if [[-n "$PYTHON_PYPI_GITLAB_GROUP_ID"]] ; then&lt;/span&gt;
        &lt;span class="s"&gt;export PYTHON_PYPI_DOWNLOAD_URL="https://${PYTHON_PYPI_USERNAME}:${PYTHON_PYPI_PASSWORD}@${CI_SERVER_HOST}/api/v4/groups/${PYTHON_PYPI_GITLAB_GROUP_ID}/-/packages/pypi/simple"&lt;/span&gt;
        &lt;span class="s"&gt;echo "Pulling PyPI packages from GitLab group ID $PYTHON_PYPI_GITLAB_GROUP_ID"&lt;/span&gt;
      &lt;span class="s"&gt;elif [[-n "$PYTHON_PYPI_GITLAB_PROJECT_ID"]] ; then&lt;/span&gt;
        &lt;span class="s"&gt;export PYTHON_PYPI_DOWNLOAD_URL="https://${PYTHON_PYPI_USERNAME}:${PYTHON_PYPI_PASSWORD}@${CI_SERVER_HOST}/api/v4/projects/${PYTHON_PYPI_GITLAB_PROJECT_ID}/packages/pypi/simple"&lt;/span&gt;
        &lt;span class="s"&gt;echo "Pulling PyPI packages from GitLab project ID $PYTHON_PYPI_GITLAB_PROJECT_ID"&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;if [[-n "$PYTHON_PYPI_DOWNLOAD_URL"]] ; then&lt;/span&gt;
      &lt;span class="s"&gt;cat &amp;gt; "$PIP_CONFIG_FILE" &amp;lt;&amp;lt;EOF&lt;/span&gt;
      &lt;span class="s"&gt;[global]&lt;/span&gt;
      &lt;span class="s"&gt;index-url = ${PYTHON_PYPI_DOWNLOAD_URL}&lt;/span&gt;
      &lt;span class="s"&gt;EOF&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$PYTHON -m pip install pyroma&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$PYTHON -m pyroma -n "$PYTHON_PYROMA_MINIMUM_RATING" .&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;setup.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above job is fully expanded and no longer depends on another CI file.&lt;/p&gt;

&lt;p&gt;Adopting &lt;code&gt;cici bundle&lt;/code&gt; on your own project isn't very complex. The first thing you'll need to do is move your existing shared CI file into a new &lt;code&gt;.cici/&lt;/code&gt; directory as &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .cici/
git &lt;span class="nb"&gt;mv &lt;/span&gt;include.yml .cici/.gitlab-ci.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contents of your CI file can mostly stay the same, with the caveat that &lt;code&gt;cici&lt;/code&gt; ignores hidden jobs (those that start with &lt;code&gt;.&lt;/code&gt;), so those ones will not be bundled.&lt;/p&gt;

&lt;p&gt;Ensure that every job in your CI file starts with the path of the project. For example, for &lt;code&gt;brettops/pipelines/ansible&lt;/code&gt;, the jobs must start with &lt;code&gt;ansible-&lt;/code&gt;. This restriction may be lifted in a future release.&lt;/p&gt;

&lt;p&gt;It is also wise to prefix all global variables with the project path. So for &lt;code&gt;brettops/pipelines/ansible&lt;/code&gt;, your variables should start with &lt;code&gt;ANSIBLE_&lt;/code&gt; (though this is not currently enforced by the tool).&lt;/p&gt;

&lt;p&gt;Now you can run &lt;code&gt;cici bundle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cici bundle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cici bundle
&lt;span class="go"&gt;pipeline name: python
bundle names: ['black', 'isort', 'mypy', 'pyroma', 'pytest', 'setuptools', 'twine', 'vulture']
created black.yml
created isort.yml
created mypy.yml
created pyroma.yml
created pytest.yml
created setuptools.yml
created twine.yml
created vulture.yml
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As noted above, this creates new CI files. Add the pre-commit hook to your project, and your CI bundles will be rebuilt every time you try to commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .pre-commit-config.yaml&lt;/span&gt;
&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# other hooks ...&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://gitlab.com/brettops/tools/cici-tools&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.2.5"&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can use as many or as few components of your reusable CI file as you like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .gitlab-ci.yml&lt;/span&gt;
&lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;brettops/pipelines/python&lt;/span&gt;
    &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;black.yml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;isort.yml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mypy.yml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pyroma.yml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pytest.yml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;setuptools.yml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;twine.yml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;vulture.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...which is awesome!&lt;/p&gt;

&lt;p&gt;This tool is definitely still in development, so there is, again, no guarantee that it will work for any particular use case. Also, not all GitLab CI syntax is supported yet, but &lt;code&gt;cici&lt;/code&gt; will loudly complain when it encounters syntax it doesn't recognize. Here is the &lt;a href="https://gitlab.com/brettops/tools/cici-tools/-/blob/main/docs/providers/gitlab.md" rel="noopener noreferrer"&gt;currently supported syntax&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'm slowly working to adopt &lt;code&gt;cici-tools&lt;/code&gt; across the BrettOps pipeline catalog, starting with the more complex ones that really, &lt;em&gt;really&lt;/em&gt; need this functionality. The old shared pipeline files are much harder to maintain, and while they will be kept around on a best-effort basis, it is highly recommended to transition over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;These three tools make up the &lt;code&gt;cici&lt;/code&gt; command currently, but there are more on the way. A lot of possibilities have opened up by having a GitLab CI &lt;a href="https://brettops.io/blog/loading-config-files-python/#structurize-into-models" rel="noopener noreferrer"&gt;structurizer&lt;/a&gt; written in Python, which means I can now manipulate CI files all day long, in a type-safe, immutable way, using my favorite programming language.&lt;/p&gt;

&lt;p&gt;Over the years, I've written a lot of scattered tools and scripts to perform automated edits and analyses to GitLab CI files, but I anticipate this effort will unify my approach a lot. Things that I expect to fall out of this effort include, but are not limited to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Automatic pinning of job image versions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Extensions to GitLab CI, like sourcing job scripts from standalone bash scripts rather than only YAML files&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bundle-time resolution of included CI pipelines to reduce blast radius of bad pipeline changes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Converting GitLab CI/CD to / from other formats to some degree, both to provide an onramp onto GitLab from other CI systems, and to use GitLab CI syntax as a "write once, run everywhere" format&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simulating a mostly complete GitLab CI pipeline locally&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Who knows what will come next, but I'm excited to see where this tool goes. Drop me a line at &lt;a href="mailto:contact@brettops.io"&gt;&lt;/a&gt;&lt;a href="mailto:contact@brettops.io"&gt;contact@brettops.io&lt;/a&gt; to tell me what you think! Happy coding!&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>gitlab</category>
      <category>pipelines</category>
      <category>yaml</category>
    </item>
    <item>
      <title>Customize a Raspberry Pi image without any hardware</title>
      <dc:creator>Brett Weir</dc:creator>
      <pubDate>Mon, 26 Jun 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/brettops/customize-a-raspberry-pi-image-without-any-hardware-7a1</link>
      <guid>https://dev.to/brettops/customize-a-raspberry-pi-image-without-any-hardware-7a1</guid>
      <description>&lt;p&gt;Customizing SD card images is the worst—having to boot the device over and over again, and then grabbing the SD card to copy it back to your system, only to repeat it all over again the next time you need to make a change. Bleh.&lt;/p&gt;

&lt;p&gt;Beyond that, this causes all kinds of problems in your operations story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;How would you reproduce your card image if you lost it?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How would you patch, upgrade, or otherwise modify the base image?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How do you know what is actually being delivered to customers?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm here to tell you that there's a better way! In this article, I'll show you a method for making all the changes you want to your embedded Linux operating system image without any actual hardware, even if the target architecture is different.&lt;/p&gt;

&lt;p&gt;Yes, I am serious.&lt;/p&gt;

&lt;p&gt;You'll learn how to prepare a &lt;a href="https://man7.org/linux/man-pages/man2/chroot.2.html" rel="noopener noreferrer"&gt;&lt;code&gt;chroot&lt;/code&gt;&lt;/a&gt; environment that will allow you to run commands inside your Raspberry Pi image as if you had the hardware running at your desk. It'll be possible to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Install packages,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Customize the boot config,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set up SSH keys,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And who knows what else!&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only limit is your imagination, and you can do it all right from the comfort of your computer.&lt;/p&gt;

&lt;p&gt;This recipe targets Raspberry Pi because of its ubiquity, but can be adapted to almost any SD card image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A &lt;a href="https://www.virtualbox.org/" rel="noopener noreferrer"&gt;VirtualBox&lt;/a&gt; environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.vagrantup.com/" rel="noopener noreferrer"&gt;Vagrant&lt;/a&gt; installed.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Set up a Vagrant box
&lt;/h2&gt;

&lt;p&gt;We'll complete the steps in this article inside a &lt;a href="https://www.vagrantup.com/" rel="noopener noreferrer"&gt;Vagrant&lt;/a&gt; virtual machine, because when you make mistakes while dealing with filesystem images, chances are, you'll end up with hung devices and needing to reboot often to recover.&lt;/p&gt;

&lt;p&gt;Using Vagrant allows us to reboot a virtual machine instead and not disrupt our flow. It also means that you can complete this tutorial on any host machine, running Linux or not.&lt;/p&gt;

&lt;p&gt;Create the following &lt;code&gt;Vagrantfile&lt;/code&gt; in an empty directory, modifiying the values for &lt;code&gt;vb.cpus&lt;/code&gt; and &lt;code&gt;vb.memory&lt;/code&gt; as needed. Be sure to give the box as much oomph as you can spare, as your computer will get hungry when compressing and uncompressing the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Vagrantfile&lt;/span&gt;
&lt;span class="no"&gt;Vagrant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ubuntu/jammy64"&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"virtualbox"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;vb&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;vb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="n"&gt;vb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"4096"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the &lt;code&gt;Vagrantfile&lt;/code&gt; in place, start your Vagrant box and log in to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vagrant up
vagrant ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vagrant up
&lt;span class="go"&gt;Bringing machine 'default' up with 'virtualbox' provider...
&lt;/span&gt;&lt;span class="gp"&gt;==&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;default: Importing base box &lt;span class="s1"&gt;'ubuntu/jammy64'&lt;/span&gt;...
&lt;span class="gp"&gt;==&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;default: Matching MAC address &lt;span class="k"&gt;for &lt;/span&gt;NAT networking...
&lt;span class="gp"&gt;==&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;default: Checking &lt;span class="k"&gt;if &lt;/span&gt;box &lt;span class="s1"&gt;'ubuntu/jammy64'&lt;/span&gt; version &lt;span class="s1"&gt;'20230302.0.0'&lt;/span&gt; is up to date...
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="c"&gt;...
...
&lt;/span&gt;&lt;span class="gp"&gt;    default: /vagrant =&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/brett/Projects/examples/card-image-ci
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vagrant ssh
&lt;span class="go"&gt;Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-67-generic x86_64)

 * Documentation: https://help.ubuntu.com
 * Management: https://landscape.canonical.com
 * Support: https://ubuntu.com/advantage
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Download the image
&lt;/h2&gt;

&lt;p&gt;Go to the &lt;a href="https://www.raspberrypi.com/software/operating-systems/" rel="noopener noreferrer"&gt;Raspberry Pi download page&lt;/a&gt; and find one you like. I chose &lt;strong&gt;Raspberry Pi OS Lite&lt;/strong&gt; because the download is much smaller and I can add whatever I like to it. I also chose 64-bit because I have a newer Raspberry Pi.&lt;/p&gt;

&lt;p&gt;You can download the exact image I chose by doing the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget &lt;span class="nt"&gt;--progress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bar:noscroll https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf-lite.img.xz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;wget &lt;span class="nt"&gt;--progress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bar:noscroll https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf-lite.img.xz
&lt;span class="go"&gt;--2023-06-24 02:57:30-- https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf-lite.img.xz
Resolving downloads.raspberrypi.org (downloads.raspberrypi.org)... 176.126.240.86, 46.235.230.122, 93.93.135.117, ...
Connecting to downloads.raspberrypi.org (downloads.raspberrypi.org)|176.126.240.86|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 381558864 (364M) [application/x-xz]
Saving to: ‘2023-05-03-raspios-bullseye-armhf-lite.img.xz’

&lt;/span&gt;&lt;span class="gp"&gt;2023-05-03-raspios-bullseye-armhf-lite. 100%[=============================================================================&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; 363.88M 9.43MB/s &lt;span class="k"&gt;in &lt;/span&gt;43s
&lt;span class="go"&gt;
2023-06-24 02:58:14 (8.44 MB/s) - ‘2023-05-03-raspios-bullseye-armhf-lite.img.xz’ saved [381558864/381558864]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Set up the filesystem
&lt;/h2&gt;

&lt;p&gt;With the image downloaded, you're ready to prepare the image for access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Uncompress the image
&lt;/h3&gt;

&lt;p&gt;The first step is to uncompress the image if needed. The Raspberry Pi image downloaded above is compressed in the &lt;code&gt;.xz&lt;/code&gt; format, so you'll need the &lt;a href="https://linux.die.net/man/1/xz" rel="noopener noreferrer"&gt;&lt;code&gt;xz&lt;/code&gt; and &lt;code&gt;unxz&lt;/code&gt; commands&lt;/a&gt;. These should be pre-installed on the Vagrant box, but if they are not, they're easy to install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; xz-utils
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uncompress the image. You can add the &lt;code&gt;-v&lt;/code&gt; flag to print the progress in real-time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unxz &lt;span class="nt"&gt;-v&lt;/span&gt; 2023-05-03-raspios-bullseye-armhf-lite.img.xz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;unxz &lt;span class="nt"&gt;-v&lt;/span&gt; 2023-05-03-raspios-bullseye-armhf-lite.img.xz
&lt;span class="go"&gt;2023-05-03-raspios-bullseye-armhf-lite.img.xz (1/1)
  100 % 363.9 MiB / 1876.0 MiB = 0.194 44 MiB/s 0:42
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can tell it's uncompressed because it's now enormous, lol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-hs&lt;/span&gt; 2023-05-03-raspios-bullseye-armhf-lite.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-hs&lt;/span&gt; 2023-05-03-raspios-bullseye-armhf-lite.img
&lt;span class="go"&gt;1.3G 2023-05-03-raspios-bullseye-armhf-lite.img
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Resize the image (optional)
&lt;/h3&gt;

&lt;p&gt;If you're going to customize an OS image, you'll probably hit the existing disk size limit pretty quickly.&lt;/p&gt;

&lt;p&gt;Install the amazing &lt;a href="https://www.qemu.org/docs/master/tools/qemu-img.html" rel="noopener noreferrer"&gt;&lt;code&gt;qemu-img&lt;/code&gt;&lt;/a&gt; utility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; qemu-utils
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect the image to find out what you're working with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;qemu-img info 2023-05-03-raspios-bullseye-armhf-lite.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;qemu-img info 2023-05-03-raspios-bullseye-armhf-lite.img
&lt;span class="go"&gt;image: 2023-05-03-raspios-bullseye-armhf-lite.img
file format: raw
virtual size: 1.83 GiB (1967128576 bytes)
disk size: 1.37 GiB

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you wanted to install, let's say, LibreOffice or something, it would need to be quite a bit larger. Let's resize the image file itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;qemu-img resize 2023-05-03-raspios-bullseye-armhf-lite.img +2G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;qemu-img resize &lt;span class="nt"&gt;-f&lt;/span&gt; raw 2023-05-03-raspios-bullseye-armhf-lite.img +2G
&lt;span class="go"&gt;Image resized.

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that only resizes the image. You still need to grow the root partition that has all the stuff, and then expand the filesystem to fill the partition.&lt;/p&gt;

&lt;p&gt;Let's find out which partition is which:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fdisk &lt;span class="nt"&gt;-l&lt;/span&gt; 2023-05-03-raspios-bullseye-armhf-lite.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fdisk &lt;span class="nt"&gt;-l&lt;/span&gt; 2023-05-03-raspios-bullseye-armhf-lite.img
&lt;span class="go"&gt;Disk 2023-05-03-raspios-bullseye-armhf-lite.img: 3.83 GiB, 4114612224 bytes, 8036352 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x4c4e106f

Device Boot Start End Sectors Size Id Type
2023-05-03-raspios-bullseye-armhf-lite.img1 8192 532479 524288 256M c W95 FAT32 (LBA)
2023-05-03-raspios-bullseye-armhf-lite.img2 532480 3842047 3309568 1.6G 83 Linux
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks like the second partition is what we want. Let's use the &lt;code&gt;growpart&lt;/code&gt; command to expand it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;growpart 2023-05-03-raspios-bullseye-armhf-lite.img 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;growpart 2023-05-03-raspios-bullseye-armhf-lite.img 2
&lt;span class="go"&gt;CHANGED: partition=2 start=532480 old: size=3309568 end=3842048 new: size=7503839 end=8036319
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fdisk&lt;/code&gt; will now report the expanded size, which grew from &lt;code&gt;1.6G&lt;/code&gt; to &lt;code&gt;3.6G&lt;/code&gt;, as expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fdisk &lt;span class="nt"&gt;-l&lt;/span&gt; 2023-05-03-raspios-bullseye-armhf-lite.img
&lt;span class="go"&gt;Disk 2023-05-03-raspios-bullseye-armhf-lite.img: 3.83 GiB, 4114612224 bytes, 8036352 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x4c4e106f

Device Boot Start End Sectors Size Id Type
2023-05-03-raspios-bullseye-armhf-lite.img1 8192 532479 524288 256M c W95 FAT32 (LBA)
2023-05-03-raspios-bullseye-armhf-lite.img2 532480 8036318 7503839 3.6G 83 Linux
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later on, after we mount the loopback devices, we'll be able to grow the filesystem to use the newly available space.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add loopback devices
&lt;/h3&gt;

&lt;p&gt;The next tool you're going to need is the &lt;a href="https://linux.die.net/man/8/losetup" rel="noopener noreferrer"&gt;&lt;code&gt;losetup&lt;/code&gt; command&lt;/a&gt;, which allows you to manage loopback devices. We'll use it to create new loopback devices for the card image partitions and mount them to directories.&lt;/p&gt;

&lt;p&gt;In my case, the first available device was &lt;code&gt;/dev/loop6&lt;/code&gt;, but your specific device number may differ whenever this command is called. The &lt;code&gt;losetup&lt;/code&gt; command returns the loopback device path, so we'll assign it to a variable to avoid referring to it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DEVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;losetup &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--show&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt; 2023-05-03-raspios-bullseye-armhf-lite.img&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$DEVICE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;DEVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;losetup &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--show&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt; 2023-05-03-raspios-bullseye-armhf-lite.img&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$DEVICE&lt;/span&gt;
&lt;span class="go"&gt;/dev/loop6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use &lt;code&gt;lsblk&lt;/code&gt; to inspect the newly available loopback devices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;lsblk &lt;span class="nt"&gt;-o&lt;/span&gt; name,label,size &lt;span class="nv"&gt;$DEVICE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;lsblk &lt;span class="nt"&gt;-o&lt;/span&gt; name,label,size &lt;span class="nv"&gt;$DEVICE&lt;/span&gt;
&lt;span class="go"&gt;NAME LABEL SIZE
loop6 3.8G
├─loop6p1 bootfs 256M
└─loop6p2 rootfs 3.6G
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bootfs&lt;/code&gt; and &lt;code&gt;rootfs&lt;/code&gt; are classic Raspberry Pi device labels, so it's looking like we're in good shape.&lt;/p&gt;

&lt;p&gt;If you forget which device you had, run &lt;code&gt;losetup -l&lt;/code&gt; to find it again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;losetup &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;losetup &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="go"&gt;NAME SIZELIMIT OFFSET AUTOCLEAR RO BACK-FILE DIO LOG-SEC
/dev/loop1 0 0 1 1 /var/lib/snapd/snaps/lxd_24322.snap 0 512
/dev/loop4 0 0 1 1 /var/lib/snapd/snaps/snapd_19457.snap 0 512
/dev/loop2 0 0 1 1 /var/lib/snapd/snaps/core20_1950.snap 0 512
/dev/loop0 0 0 1 1 /var/lib/snapd/snaps/core20_1822.snap 0 512
/dev/loop6 0 0 0 0 /home/vagrant/2023-05-03-raspios-bullseye-armhf-lite.img 0 512
/dev/loop3 0 0 1 1 /var/lib/snapd/snaps/snapd_18357.snap 0 512

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, it's &lt;code&gt;/dev/loop6&lt;/code&gt;. Set the &lt;code&gt;$DEVICE&lt;/code&gt; variable manually to continue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DEVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/loop6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;If you opted to resize the image, you can grow the filesystem to its new size by running &lt;a href="https://linux.die.net/man/8/e2fsck" rel="noopener noreferrer"&gt;&lt;code&gt;e2fsck&lt;/code&gt;&lt;/a&gt; and then &lt;a href="https://linux.die.net/man/8/resize2fs" rel="noopener noreferrer"&gt;resize2fs&lt;/a&gt;:&lt;/p&gt;

&lt;pre data-lang="bash"&gt;
sudo e2fsck -f ${DEVICE}p2
sudo resize2fs ${DEVICE}p2
&lt;/pre&gt;

&lt;pre data-lang="console"&gt;
vagrant@ubuntu-jammy:~$ sudo e2fsck -f ${DEVICE}p2
e2fsck 1.46.5 (30-Dec-2021)
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
rootfs: 49948/103584 files (0.1% non-contiguous), 341565/413696 blocks
vagrant@ubuntu-jammy:~$ sudo resize2fs ${DEVICE}p2
resize2fs 1.46.5 (30-Dec-2021)
Resizing the filesystem on /dev/loop5p2 to 937979 (4k) blocks.
The filesystem on /dev/loop5p2 is now 937979 (4k) blocks long.
&lt;/pre&gt;

&lt;p&gt;Your filesystem is now its full size.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Mount partitions
&lt;/h3&gt;

&lt;p&gt;The disk image is ready to be mounted to your local filesystem so that it appears as any other directory would, except that it will be the root filesystem of your Raspberry Pi image. Let's go!&lt;/p&gt;

&lt;p&gt;You'll want to mirror the final filesystem layout as much as possible. That means opening up the root filesystem, finding &lt;code&gt;/etc/fstab&lt;/code&gt;, and seeing what it has first.&lt;/p&gt;

&lt;p&gt;Create the &lt;code&gt;rootfs&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; rootfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mount the &lt;code&gt;rootfs&lt;/code&gt; partition onto the &lt;code&gt;rootfs/&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEVICE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;p2 rootfs/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should now be able to inspect the filesystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/
&lt;span class="go"&gt;bin boot dev etc home lib lost+found media mnt opt proc root run sbin srv sys tmp usr var

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, you can discover the &lt;code&gt;/etc/fstab&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;rootfs/etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;rootfs/etc/fstab
&lt;span class="go"&gt;proc /proc proc defaults 0 0
PARTUUID=4c4e106f-01 /boot vfat defaults 0 2
PARTUUID=4c4e106f-02 / ext4 defaults,noatime 0 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is promising! All we're missing are the contents of the &lt;code&gt;rootfs/boot/&lt;/code&gt; directory, which we might have guessed because the &lt;code&gt;bootfs&lt;/code&gt; partition hasn't been mounted yet. Easy peasy—there's even already an empty &lt;code&gt;rootfs/boot/&lt;/code&gt; directory waiting for us, which we can make sure by doing the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/boot/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/boot/
&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So let's mount the &lt;code&gt;bootfs&lt;/code&gt; partition there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEVICE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;p1 rootfs/boot/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All the fun Raspberry Pi stuff is there now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/boot/
&lt;span class="go"&gt;COPYING.linux bcm2708-rpi-zero.dtb bcm2710-rpi-zero-2-w.dtb bootcode.bin fixup4x.dat kernel7l.img start4x.elf
LICENCE.broadcom bcm2709-rpi-2-b.dtb bcm2710-rpi-zero-2.dtb cmdline.txt fixup_cd.dat kernel8.img start_cd.elf
bcm2708-rpi-b-plus.dtb bcm2709-rpi-cm2.dtb bcm2711-rpi-4-b.dtb config.txt fixup_db.dat overlays start_db.elf
bcm2708-rpi-b-rev1.dtb bcm2710-rpi-2-b.dtb bcm2711-rpi-400.dtb fixup.dat fixup_x.dat start.elf start_x.elf
bcm2708-rpi-b.dtb bcm2710-rpi-3-b-plus.dtb bcm2711-rpi-cm4-io.dtb fixup4.dat issue.txt start4.elf
bcm2708-rpi-cm.dtb bcm2710-rpi-3-b.dtb bcm2711-rpi-cm4.dtb fixup4cd.dat kernel.img start4cd.elf
bcm2708-rpi-zero-w.dtb bcm2710-rpi-cm3.dtb bcm2711-rpi-cm4s.dtb fixup4db.dat kernel7.img start4db.elf
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mount special host filesystems
&lt;/h3&gt;

&lt;p&gt;Some commands you'll want to run, like those for installing packages, enabling services, or even connecting to the Internet, may require &lt;a href="https://unix.stackexchange.com/questions/188886/what-is-in-dev-proc-and-sys" rel="noopener noreferrer"&gt;&lt;code&gt;/dev&lt;/code&gt;, &lt;code&gt;/proc&lt;/code&gt;, and &lt;code&gt;/sys&lt;/code&gt; filesystems&lt;/a&gt; to be mounted inside the &lt;code&gt;rootfs/&lt;/code&gt; directory. They should currently be empty, except for &lt;code&gt;rootfs/dev/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/sys/
&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/proc/
&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/dev/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/sys/
&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/proc/
&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/dev/
&lt;span class="go"&gt;console fd full null ptmx pts random shm stderr stdin stdout tty urandom zero
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you've confirmed those directories exist and are empty, mount the filesystems by doing the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-t&lt;/span&gt; proc /proc rootfs/proc/
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;--bind&lt;/span&gt; /sys rootfs/sys/
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;--bind&lt;/span&gt; /dev rootfs/dev/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now those directories will be populated with a ton of magic stuff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/sys/
&lt;span class="go"&gt;block bus class dev devices firmware fs hypervisor kernel module power
&lt;/span&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/proc/
&lt;span class="go"&gt;1 112 131 175 221 32 408 645 852 98 diskstats kallsyms misc slabinfo version_signature
10 1131 14 18 24 33 409 646 853 99 dma kcore modules softirqs vmallocinfo
102 1132 15 187 25 34 410 649 86 acpi driver key-users mounts stat vmstat
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;rootfs/dev/
&lt;span class="go"&gt;autofs fd loop3 port shm tty15 tty28 tty40 tty53 tty9 ttyS20 ttyS5 vcs3 vcsu3
block full loop3p1 ppp snapshot tty16 tty29 tty41 tty54 ttyS0 ttyS21 ttyS6 vcs4 vcsu4
bsg fuse loop3p2 psaux snd tty17 tty3 tty42 tty55 ttyS1 ttyS22 ttyS7 vcs5 vcsu5
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Enable ARM virtualization
&lt;/h2&gt;

&lt;p&gt;At this point, we're &lt;em&gt;almost&lt;/em&gt; ready to &lt;code&gt;chroot&lt;/code&gt; into the environment. If we tried to use the Pi filesystem right now, we'd discover that none of the commands work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./rootfs/bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./rootfs/bin/bash
&lt;span class="go"&gt;-bash: ./rootfs/bin/bash: cannot execute binary file: Exec format error

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's because the Raspberry Pi is an ARM platform, and our machine is x86-64.&lt;/p&gt;

&lt;p&gt;That's okay though. We can add &lt;code&gt;qemu-user-static&lt;/code&gt; to the filesystem so that ARM binaries run via QEMU when executed on our system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; qemu-user-static
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we run the ARM &lt;code&gt;bash&lt;/code&gt; command again, we'll get... a different error!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./rootfs/bin/bash
&lt;span class="go"&gt;arm-binfmt-P: Could not open '/lib/ld-linux-armhf.so.3': No such file or directory
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's good. This error is different and expected because the &lt;code&gt;bash&lt;/code&gt; binary is dynamically linked and has no way of linking to its nice ARM friends right now. We'll fix that next, when we finally &lt;code&gt;chroot&lt;/code&gt; into our system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access the filesystem
&lt;/h2&gt;

&lt;p&gt;It's the moment you've been waiting for. Let's run our fake Raspberry Pi!&lt;/p&gt;

&lt;p&gt;You can "log in" to your filesystem with &lt;code&gt;chroot&lt;/code&gt;, which will start an instance of the default shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chroot &lt;/span&gt;rootfs/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo chroot &lt;/span&gt;rootfs/
&lt;span class="gp"&gt;root@ubuntu-jammy:/#&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wow, so cool! I can go home sweet home!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@ubuntu-jammy:/#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;
&lt;span class="gp"&gt;root@ubuntu-jammy:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="gp"&gt;root@ubuntu-jammy:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;
&lt;span class="go"&gt;/root
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can more or less use it as though it were my machine. Give it a try.&lt;/p&gt;

&lt;p&gt;You can run &lt;a href="https://www.raspberrypi.com/documentation/computers/configuration.html" rel="noopener noreferrer"&gt;&lt;code&gt;raspi-config&lt;/code&gt;&lt;/a&gt;, because why not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;raspi-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ot59qr6kro3ri3veq2j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ot59qr6kro3ri3veq2j.png" alt="Running  raw `raspi-config` endraw  on my " width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can install &lt;code&gt;vim&lt;/code&gt;, because it's &lt;em&gt;everyone's&lt;/em&gt; favorite code editor, right?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get update &lt;span class="nt"&gt;-y&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; vim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyt4rk4tvawv74gl1ug44.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyt4rk4tvawv74gl1ug44.png" alt="Installing  raw `vim` endraw  on my " width="800" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can edit the hostname:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"my-cool-computer"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/hostname
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/hostname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@ubuntu-jammy:/#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/hostname
&lt;span class="go"&gt;old-hostname
&lt;/span&gt;&lt;span class="gp"&gt;root@ubuntu-jammy:/#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"my-cool-computer"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/hostname
&lt;span class="gp"&gt;root@ubuntu-jammy:/#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/hostname
&lt;span class="go"&gt;my-cool-computer
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can exit the &lt;code&gt;chroot&lt;/code&gt; environment with &lt;code&gt;exit&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@ubuntu-jammy:/#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;span class="go"&gt;exit
&lt;/span&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pretty much anything that you could configure on the actual device can now be configured beforehand without the need to set up any hardware, which is pretty great.&lt;/p&gt;

&lt;p&gt;It doesn't stop at hand edits either. You can pipe scripts into the &lt;code&gt;chroot&lt;/code&gt; environment, if you have a set of tasks that you need to run often:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ls /etc"&lt;/span&gt; | &lt;span class="nb"&gt;sudo chroot &lt;/span&gt;rootfs/ bash -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ls -1 /etc"&lt;/span&gt; | &lt;span class="nb"&gt;sudo chroot &lt;/span&gt;rootfs/ bash -
&lt;span class="go"&gt;NetworkManager
X11
adduser.conf
alternatives
apparmor.d
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or you can get even fancier. Ansible, for example, has a &lt;a href="https://docs.ansible.com/ansible/latest/collections/community/general/chroot_connection.html" rel="noopener noreferrer"&gt;&lt;code&gt;chroot&lt;/code&gt; connection plugin&lt;/a&gt;, so you can run Ansible playbooks to configure your Pi filesystem—again, without the need for actual hardware.&lt;/p&gt;

&lt;p&gt;Anyway, let's assume we've set everything up the way we want. Yay! But we're not done yet. This filesystem won't do us any good until we repack it to be actually copied onto a real SD card. That comes next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tear down the filesystem
&lt;/h2&gt;

&lt;p&gt;We now need to repeat everything we just did, but in the reverse order to get back to a packed-up, ready-to-ship card image.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unmount filesystems
&lt;/h3&gt;

&lt;p&gt;Unmount all the special filesystems we mounted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;umount rootfs/dev/
&lt;span class="nb"&gt;sudo &lt;/span&gt;umount rootfs/sys/
&lt;span class="nb"&gt;sudo &lt;/span&gt;umount rootfs/proc/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unmount the loopback devices for the &lt;code&gt;bootfs&lt;/code&gt; and &lt;code&gt;rootfs&lt;/code&gt; partitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;umount rootfs/boot/
&lt;span class="nb"&gt;sudo &lt;/span&gt;umount rootfs/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Delete loopback devices
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;losetup&lt;/code&gt; to detach the loopback devices that you created earlier to sever the final link between your disk image and your host operating system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;losetup &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;$DEVICE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Compress the image
&lt;/h3&gt;

&lt;p&gt;Now that everything is disconnected, you can repack the SD card image with &lt;code&gt;xz&lt;/code&gt;. This might take a significant amount of time, during which time your computer will purr like an angry kitten. Be patient, and add the &lt;code&gt;-v&lt;/code&gt; flag if you like having literally any feedback on what's happening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xz &lt;span class="nt"&gt;-v&lt;/span&gt; 2023-05-03-raspios-bullseye-armhf-lite.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;xz &lt;span class="nt"&gt;-v&lt;/span&gt; 2023-05-03-raspios-bullseye-armhf-lite.img
&lt;span class="go"&gt;2023-05-03-raspios-bullseye-armhf-lite.img (1/1)
  0.8 % 26.3 MiB / 32.3 MiB = 0.815 3.2 MiB/s 0:10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eventually, it should finish. And then, huzzah! Look, our 3.6G image is compressed to 421MB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-hs&lt;/span&gt; 2023-05-03-raspios-bullseye-armhf-lite.img.xz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-hs&lt;/span&gt; 2023-05-03-raspios-bullseye-armhf-lite.img.xz
&lt;span class="go"&gt;421M 2023-05-03-raspios-bullseye-armhf-lite.img.xz
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ship it!
&lt;/h2&gt;

&lt;p&gt;Your disk image is now in the exact same state that it was in when you downloaded it, &lt;strong&gt;except that it's actually not&lt;/strong&gt;. The filesystem is now much larger and has all your super cool modifications ready to deploy to devices &lt;em&gt;anywhere in the world&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Upload it to your FTP server, post it on USENET, or even put it on an actual physical SD card. Whatever you do, don't keep it to yourself, because the world &lt;strong&gt;needs&lt;/strong&gt; your custom distribution of &lt;a href="https://sonic-pi.net/" rel="noopener noreferrer"&gt;Sonic Pi&lt;/a&gt;, or whatever it was that you needed to customize your Pi image for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;If you want to save the card image you created, move it into the &lt;code&gt;/vagrant/&lt;/code&gt; directory. It will then be in the same directory as your &lt;code&gt;Vagrantfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.xz /vagrant/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then exit the &lt;code&gt;vagrant ssh&lt;/code&gt; session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;vagrant@ubuntu-jammy:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;span class="go"&gt;exit
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, destroy the box you created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vagrant destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The tools and techniques discussed in this article are generally applicable to all kinds of embedded Linux disk images, not just Raspberry Pi. Once you know the filesystem layout, anything is possible!&lt;/p&gt;

&lt;p&gt;You can even take all these instructions and dump them into a CI pipeline so that you never have to do this ever again!&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>automation</category>
      <category>embedded</category>
      <category>linux</category>
      <category>raspberrypi</category>
    </item>
    <item>
      <title>Hosting static sites with Cloudflare R2 and MinIO Client</title>
      <dc:creator>Brett Weir</dc:creator>
      <pubDate>Mon, 19 Jun 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/brettops/hosting-static-sites-with-cloudflare-r2-and-minio-client-21p3</link>
      <guid>https://dev.to/brettops/hosting-static-sites-with-cloudflare-r2-and-minio-client-21p3</guid>
      <description>&lt;p&gt;There are countless services nowadays for hosting static sites: &lt;a href="https://pages.github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, &lt;a href="https://docs.gitlab.com/ee/user/project/pages/" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;, Netlify, &lt;a href="https://surge.sh/" rel="noopener noreferrer"&gt;Surge&lt;/a&gt;, &lt;a href="https://kb.porkbun.com/article/137-how-to-set-up-static-hosting" rel="noopener noreferrer"&gt;Porkbun&lt;/a&gt;, &lt;a href="https://docs.digitalocean.com/products/app-platform/how-to/manage-static-sites/" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt;, even &lt;a href="https://pages.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If there are so many ways to get a static site online, why would anyone bother with setting up a plain ol' S3 bucket?&lt;/p&gt;

&lt;p&gt;Well, there are lots of reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hosting multiple versions of a site&lt;/strong&gt;. If you want &lt;code&gt;v1&lt;/code&gt;, &lt;code&gt;v2&lt;/code&gt;, and &lt;code&gt;v3&lt;/code&gt; at the same time, but &lt;strong&gt;don't&lt;/strong&gt; want to commit your built sites to Git, then you need a writable location.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hosting many sites from one domain&lt;/strong&gt;. Maybe you're a hosting service! Or maybe you just provide hosting for multiple users in your company. You can build out a consistent workflow on top of S3 and host all the content in a single location.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Total control of how the sites are published&lt;/strong&gt;. Maybe you want to vary available content by region, add authentication, use server-side analytics, or just configure how content is cached by the CDN. Building your own custom workflow will give you access to all the levers you need.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, we'll develop a recipe for using &lt;a href="https://www.cloudflare.com/products/r2/" rel="noopener noreferrer"&gt;Cloudflare R2&lt;/a&gt; as a static site hosting service. You will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create a simple static site, &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Publish the site to Cloudflare R2 with MinIO Client, and&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use Cloudflare Transform Rules to make your bucket behave more like a web server.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end of it, you will be the proud owner of a many-headed static site hydra that you'd never know was a simple S3 bucket underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;The easiest way to meet all the prerequisites for this tutorial is to complete the previous tutorial in this series, &lt;a href="https://brettops.io/blog/static-assets-cloudflare-r2/" rel="noopener noreferrer"&gt;Serve static assets with Cloudflare R2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's a summary of the things you'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A domain name proxied by Cloudflare.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Cloudflare account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An R2 bucket configured for public access.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;For this tutorial, I created an R2 bucket called &lt;code&gt;sites&lt;/code&gt; and made it accessible at &lt;a href="https://sites.brettops.io/" rel="noopener noreferrer"&gt;&lt;code&gt;sites.brettops.io&lt;/code&gt;&lt;/a&gt;. The domains and paths you will use for this article will be different, but the steps should otherwise be the same for you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 0: Build a static site (optional)
&lt;/h2&gt;

&lt;p&gt;If you came here because you already have a static site that you're ready to publish, use that and skip this section. For everyone else, you can set up an example site with me.&lt;/p&gt;

&lt;p&gt;I'll be using &lt;a href="https://www.mkdocs.org/" rel="noopener noreferrer"&gt;MkDocs&lt;/a&gt;, because it's fast and simple and generates some nice boilerplate so that the site isn't completely empty. MkDocs is written in Python, so you'll need Python installed (which probably isn't an issue if you're on Linux).&lt;/p&gt;

&lt;p&gt;Install the &lt;code&gt;mkdocs&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install mkdocs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This installs the &lt;code&gt;mkdocs&lt;/code&gt; command. You can test that &lt;code&gt;mkdocs&lt;/code&gt; is available by doing the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdocs --version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mkdocs --version
mkdocs, version 1.4.2 from /home/brett/.pyenv/versions/3.10.7/lib/python3.10/site-packages/mkdocs (Python 3.10)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a new &lt;code&gt;mkdocs.yml&lt;/code&gt; project in the current directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdocs new .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mkdocs new .
INFO - Writing config file: ./mkdocs.yml
INFO - Writing initial docs: ./docs/index.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more thing: let's add a subpage for this site. You'll see why this is important later in the article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat &amp;gt; docs/about.md &amp;lt;&amp;lt;EOF
# About

Some more info about that.
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run a local dev server to see your changes in action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdocs serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mkdocs serve
INFO - Building documentation...
INFO - Cleaning site directory
INFO - Documentation built in 0.05 seconds
INFO - [23:22:45] Watching paths for changes: 'docs', 'mkdocs.yml'
INFO - [23:22:45] Serving on http://127.0.0.1:8000/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit your local dev server at &lt;a href="http://127.0.0.1:8000/" rel="noopener noreferrer"&gt;http://127.0.0.1:8000/&lt;/a&gt;. Our fancy new docs site isn't going to be anything to write home about, but it'll do the job.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frsid9ijhpc03z4no0qnh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frsid9ijhpc03z4no0qnh.png" alt="New MkDocs static site served locally." width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you're satisfied with your site, you can build a finished site for hosting like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdocs build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mkdocs build
INFO - Cleaning site directory
INFO - Building documentation to directory: /home/brett/Projects/examples/mkdocs-site/site
INFO - Documentation built in 0.05 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create a &lt;code&gt;site/&lt;/code&gt; directory that contains our finished site, which is what we'll publish to Cloudflare R2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Deploy the site with MinIO Client
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/minio/mc" rel="noopener noreferrer"&gt;MinIO Client&lt;/a&gt; is far and away the best S3 command line tool I've found.&lt;/p&gt;

&lt;p&gt;It's written in Go, so getting it onto your system is easy, and it supports &lt;a href="https://min.io/docs/minio/linux/reference/minio-mc.html" rel="noopener noreferrer"&gt;a ton of commands&lt;/a&gt; that alternatives such as &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/s3/#available-commands" rel="noopener noreferrer"&gt;&lt;code&gt;aws s3&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://s3tools.org/usage" rel="noopener noreferrer"&gt;&lt;code&gt;s3cmd&lt;/code&gt;&lt;/a&gt; simply don't have.&lt;/p&gt;

&lt;p&gt;First, download and install the &lt;code&gt;mc&lt;/code&gt; tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -O https://dl.min.io/client/mc/release/linux-amd64/mc
sudo install mc /usr/local/bin/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -O https://dl.min.io/client/mc/release/linux-amd64/mc
  % Total % Received % Xferd Average Speed Time Time Time Current
                                 Dload Upload Total Spent Left Speed
100 24.9M 100 24.9M 0 0 10.2M 0 0:00:02 0:00:02 --:--:-- 10.2M
$ sudo install mc /usr/local/bin/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;For more installation options, see the &lt;a href="https://min.io/docs/minio/linux/reference/minio-mc.html#quickstart" rel="noopener noreferrer"&gt;official quickstart&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;mc&lt;/code&gt; command should be usable at this point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mc --version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mc --version
mc version RELEASE.2023-06-15T15-08-26Z (commit-id=bf3924b58341eb7a71785653a29bf26ca9fac95e)
Runtime: go1.19.10 linux/amd64
Copyright (c) 2015-2023 MinIO, Inc.
License GNU AGPLv3 &amp;lt;https://www.gnu.org/licenses/agpl-3.0.html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mc&lt;/code&gt; allows you to configure a connection by creating an &lt;strong&gt;alias&lt;/strong&gt;. You can have as many aliases configured as desired.&lt;/p&gt;

&lt;p&gt;Use the &lt;code&gt;mc alias set&lt;/code&gt; command to configure your R2 connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mc alias set NAME https://XXXXXX.r2.cloudflarestorage.com/ YYYYYY ZZZZZZ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;NAME&lt;/code&gt; is the desired name for your alias. You'll have to type this often, so it's better to keep it short. I'll call mine &lt;code&gt;r2&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;XXXXXX&lt;/code&gt; is your Cloudflare account ID.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;YYYYYY&lt;/code&gt; is your Cloudflare R2 access key ID.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ZZZZZZ&lt;/code&gt; is your Cloudflare R2 secret access key.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you've configured an alias, you can test it out or access it by prefixing the desired path with the alias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mc ls r2/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mc ls r2/
[2023-06-17 18:38:11 UTC] 0B sites/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hey, that's the &lt;code&gt;sites&lt;/code&gt; bucket! Let's try accessing it!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mc ls r2/sites/
$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above prints nothing. That's good! There's nothing in the bucket!&lt;/p&gt;

&lt;p&gt;At this point, we've verified that the bucket works. Now we can put some stuff in it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;a href="https://min.io/docs/minio/linux/reference/minio-mc.html#test-the-connection" rel="noopener noreferrer"&gt;MinIO docs&lt;/a&gt; say to test the connection like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;mc admin info r2&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;This does not work with Cloudflare&lt;/strong&gt;. I don't know why.&lt;/p&gt;

&lt;p&gt;It probably has to do with the fact that AWS S3 buckets have the bucket name in the domain, whereas Cloudflare R2 buckets have the bucket name in the path. 🤔&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For hosting a static site, far and away the best tool for the job is the &lt;code&gt;mc mirror&lt;/code&gt; command, which synchronizes files between two locations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mc mirror SOURCE TARGET
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our case, we'll set it up to synchronize the local MkDocs &lt;code&gt;site/&lt;/code&gt; directory to the R2 bucket. We'll add the &lt;code&gt;--overwrite&lt;/code&gt; flag so that it overwrites existing files if there are any differences, and we'll add the &lt;code&gt;--remove&lt;/code&gt; flag so that it deletes files from the target that no longer exist in the source.&lt;/p&gt;

&lt;p&gt;This will be great for when we create a pipeline to continuously publish changes to a site.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Double-check your command before running&lt;/strong&gt;. You can very easily delete any existing data in the bucket if you're not careful with these commands.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mc mirror site r2/sites/latest/ --overwrite --remove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mc ls r2/sites/
$ mc mirror site r2/sites/latest/ --overwrite --remove
...site/sitemap.xml.gz: 1.38 MiB / 1.38 MiB ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 556.07 KiB/s 2s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we browse to the published location, we'll be able to access the individual files we just uploaded:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8ii0v5uv4qmtkf9b1xt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8ii0v5uv4qmtkf9b1xt.png" alt="Root  raw `index.html` endraw  of the published MkDocs site." width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We're not done yet though.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Rewrite trailing slashes
&lt;/h2&gt;

&lt;p&gt;You may have noticed that if you try to click on the &lt;strong&gt;About&lt;/strong&gt; link, you get an error:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdj1cucxx8dhhlakfdeng.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdj1cucxx8dhhlakfdeng.png" alt="Clicking on the **About** link leads to an error." width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Web servers these days will almost always rewrite a URL that ends in a trailing slash (&lt;code&gt;/&lt;/code&gt;) to an &lt;code&gt;index.html&lt;/code&gt; file at the same path. In other words:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://brettops.io/" rel="noopener noreferrer"&gt;https://brettops.io/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Is the same page as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://brettops.io/index.html" rel="noopener noreferrer"&gt;https://brettops.io/index.html&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This allows you to visit &lt;code&gt;sites.brettops.io/latest/&lt;/code&gt; and the contents of &lt;code&gt;sites.brettops.io/latest/index.html&lt;/code&gt;, which is how it worked when we tested our site locally. Cloudflare doesn't do this by default, which is why our About link leads to nowhere.&lt;/p&gt;

&lt;p&gt;We can tell Cloudflare to behave like this when serving our R2 site, using &lt;a href="https://developers.cloudflare.com/rules/transform/url-rewrite/" rel="noopener noreferrer"&gt;Rewrite URL Rules&lt;/a&gt;. That way, our links will work, and we'll be able to access our site at &lt;a href="https://sites.brettops.io/latest/" rel="noopener noreferrer"&gt;&lt;code&gt;sites.brettops.io/latest/&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Go to the Cloudflare dashboard for your domain, and click &lt;strong&gt;Rules&lt;/strong&gt;, then &lt;strong&gt;Transform Rules&lt;/strong&gt; in the sidebar:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foj8hgbqql2lpzyino9xf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foj8hgbqql2lpzyino9xf.png" alt="Navigate to the **Transform Rules** page." width="256" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the &lt;strong&gt;Create rule&lt;/strong&gt; button under the &lt;strong&gt;Rewrite URL&lt;/strong&gt; tab:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmcr9ry649m4godkym3mv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmcr9ry649m4godkym3mv.png" alt="Click the **Create rule** button." width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add an actually good name for your rule. It's the only way you'll be able to remember what it does without reading through your rule expressions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6q5hw5qwr8joy64jufeh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6q5hw5qwr8joy64jufeh.png" alt="Add a good name for your rule." width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;If...&lt;/strong&gt;, select &lt;strong&gt;Custom filter expression&lt;/strong&gt;, and add the following expressions to the Expression Builder with an &lt;strong&gt;And&lt;/strong&gt; between them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hostname&lt;/td&gt;
&lt;td&gt;&lt;code&gt;equals&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sites.brettops.io&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URI Path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ends with&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr2nv8mju53yyyyrld5ec.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr2nv8mju53yyyyrld5ec.png" alt="Configure the expression to match requests for the Rewrite URL rule." width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alternatively, you can edit the expression manually by clicking &lt;strong&gt;Edit expression&lt;/strong&gt; and add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(http.host eq "sites.brettops.io" and ends_with(http.request.uri.path, "/"))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under &lt;strong&gt;Then...&lt;/strong&gt;, then under &lt;strong&gt;Path&lt;/strong&gt;, select the &lt;strong&gt;Rewrite to...&lt;/strong&gt; option, select &lt;strong&gt;Dynamic&lt;/strong&gt;, and add the following expression (see screenshot image below for reference):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;concat(http.request.uri.path, "index.html")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This uses the &lt;a href="https://developers.cloudflare.com/ruleset-engine/rules-language/functions/#function-concat" rel="noopener noreferrer"&gt;&lt;code&gt;concat&lt;/code&gt; function&lt;/a&gt; to append &lt;code&gt;index.html&lt;/code&gt; to the URLs of matched request.&lt;/p&gt;

&lt;p&gt;And under &lt;strong&gt;Query&lt;/strong&gt;, select &lt;strong&gt;Preserve&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3ib7b2sq2yjxn7dckhl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3ib7b2sq2yjxn7dckhl.png" alt="Rewrite the path, but preserve the query string." width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you're ready, click &lt;strong&gt;Deploy&lt;/strong&gt;. Then your new rule will be live:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i8zhn9k7xl8pnqdcteu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i8zhn9k7xl8pnqdcteu.png" alt="The new Rewrite URL rule is live." width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point, you should be able to navigate to your site's URLs and see that they're accessible without adding &lt;code&gt;index.html&lt;/code&gt; to the path:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.brettops.io%2Fblog%2Fstatic-site-cloudflare-r2-minio-client%2Fdocs-site-rewritten.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.brettops.io%2Fblog%2Fstatic-site-cloudflare-r2-minio-client%2Fdocs-site-rewritten.png" alt="[ raw `sites.brettops.io/latest` endraw ](https://sites.brettops.io/latest) is accessible." width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7k4yqia9dhugb9074cr6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7k4yqia9dhugb9074cr6.png" alt="Following the About link takes us to the About page, no problem." width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congratulations, we've built a fully functional static site hosting service!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Cloudflare R2 is deeply integrated with Cloudflare and easy to get started with. MinIO Client makes working with S3 clean and obvious. Together, they provide a slim, bare-bones hosting solution that is highly adaptable to different needs and use cases.&lt;/p&gt;

&lt;p&gt;What's better, all of the steps taken in this article are easy to automate in a CI pipeline, allowing you to build a general solution for your team or company that scales with your users.&lt;/p&gt;

&lt;p&gt;For simple use cases, I'd still rather use an off-the-shelf solution, but this is one of those tools that you keep in your toolbox, because you never know when you're going to need it. Sometimes, all you really need is something you can hack on and make your own.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>r2</category>
      <category>static</category>
      <category>site</category>
    </item>
    <item>
      <title>The Bootstrapping Problem</title>
      <dc:creator>Brett Weir</dc:creator>
      <pubDate>Mon, 05 Jun 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/brettops/the-bootstrapping-problem-1j35</link>
      <guid>https://dev.to/brettops/the-bootstrapping-problem-1j35</guid>
      <description>&lt;p&gt;&lt;strong&gt;Infrastructure as code&lt;/strong&gt; , or IaC for short, is often touted as a solution for all the world's problems.&lt;/p&gt;

&lt;p&gt;Imagine for a moment: no more configuration drift, clean and consistent deliverables, and all operators finally on the same page. What more could anyone ask for? I myself am fanatic about IaC, and you can often find me raving about all the problems it solves.&lt;/p&gt;

&lt;p&gt;But it's not all sunshine and roses. If you've spent any time trying to manage IaC deployments, you soon discover that all that fancy infrastructure tooling requires one thing to manage them: &lt;strong&gt;infrastructure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By the time you've gotten to a point where you can even use Terraform, Kubernetes, or whatever other fancy tools, you likely already have multiple layers of deployments that are not and cannot be managed by the tools you've just adopted.&lt;/p&gt;

&lt;p&gt;This is the Catch-22, the Achilles' heel, of infrastructure development.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;Bootstrapping Problem&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is "bootstrapping"?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Bootstrapping" rel="noopener noreferrer"&gt;Bootstrapping&lt;/a&gt; in this context describes self-sustaining processes that grow in size and complexity by chaining successive stages. Examples abound:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;New programming language compilers are compiled using older compilers, which are built using older compilers, which use compilers written in other languages, etc., all the way back until someone had to first write one in machine language.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Computers boot using successively larger bootloader, which each initialize the system just enough to support loading the next larger bootloader, until the operating system is fully loaded.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Software installers often have multiple stages so that earlier installers can update the later installers themselves.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Infrastructure goes through similar stages, whereby the introduction of a new capability to the organization makes it possible to introduce other capabilities that build on top. Each new layer depends on the previous layer and cannot exist without it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The physical hardware must be working before you can install an OS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A VM instance must be running before it can be provisioned with your software.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A container runtime must be installed before you can orchestrate containers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And so on and so forth.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A case study
&lt;/h2&gt;

&lt;p&gt;Let's see an example of the Bootstrapping Problem in action. Let's say you've got three infrastructure capabilities that you'd like to implement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You want to host your own CI/CD runners&lt;/strong&gt;. Bringing CI/CD runners home reduces your reliance on a third party and provides more control and visibility into what is critical infrastructure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You want to use Terraform to deploy infrastructure&lt;/strong&gt;. &lt;a href="https://brettops.io/blog/what-is-terraform/" rel="noopener noreferrer"&gt;Adopting an immutable infrastructure strategy with Terraform&lt;/a&gt; leads to more predictable, flexible, and resilient deployments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You want to use CI/CD to run Terraform&lt;/strong&gt;. Standardizing around your CI/CD system simplifies your delivery system and makes cross-training your team members easier.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7um1u2e1q8ltoms06vvl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7um1u2e1q8ltoms06vvl.png" alt="Three capabilities to adopt." width="800" height="57"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each new capability sounds great, so you try to implement them.&lt;/p&gt;

&lt;p&gt;You'll want to use Terraform to provision the runners, which you'll want to drive from CI/CD, which requires runners to be provisioned somewhere.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7h2hs09glrhn9kh04u7j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7h2hs09glrhn9kh04u7j.png" alt="Capability dependencies." width="314" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That, my friend, is a dependency cycle. Since this dependency cycle exists in the ordering of bootstrapping requirements, I like to call this situation a &lt;strong&gt;bootstrap loop&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Bootstrap loops are problematic because they &lt;strong&gt;must&lt;/strong&gt; be resolved for bootstrapping to complete successfully. If the runners described above experience a failure, it will not be possible to use the existing Terraform automation to resolve it, because CI/CD will not be available to run it.&lt;/p&gt;

&lt;p&gt;This can result in extended and difficult-to-resolve outages as teams scramble to find bootstrapping workarounds that circumvent existing processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do bootstrap loops occur?
&lt;/h2&gt;

&lt;p&gt;Bootstrap loops do not spontaneously occur, because it is impossible to provision infrastructure from nothing when it contains a bootstrap loop. Instead, they evolve over time as a result of changes in availability of infrastructure.&lt;/p&gt;

&lt;p&gt;For the runner situation described above to occur, there must have existed another runner at some point to kick off the whole process:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3u8rtpytmex1rvyzt5tk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3u8rtpytmex1rvyzt5tk.png" alt="A bootstrap runner starts the runner bootstrap cycle." width="604" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One of a few things might have happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The team used to pay for Cloud Runner minutes, but doesn't anymore.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The team decided to tear down their old runner box after deciding that they no longer needed it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The person who ran the old runner box left the company.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whatever the reason, there used to be a failover mechanism to kickstart this loop, but that mechanism disappeared when the organization forgot why they had it.&lt;/p&gt;

&lt;p&gt;The Bootstrapping Problem can thus be described as the tendency to create bootstrap loops on accident as part of normal operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solving the Bootstrap Problem
&lt;/h2&gt;

&lt;p&gt;Preventing the introduction of bootstrap loops is essential to building a resilient infrastructure stack that can recover from failure without operator intervention.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Know the end state
&lt;/h3&gt;

&lt;p&gt;Have an end state in mind for your infrastructure. You won't get there if you don't know what you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;What tools will you use?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How will things fit together?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Where will you host?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Who needs access?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Answering these questions well requires experience. If you don't know what you need, find someone who does. It'll help you prevent expensive early mistakes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Know the order of dependencies
&lt;/h3&gt;

&lt;p&gt;If you are part of an organization of any size or complexity, it may seem like a constellation of interdependent services just &lt;em&gt;exists&lt;/em&gt; and has happily done so for all time.&lt;/p&gt;

&lt;p&gt;But this is not the case!&lt;/p&gt;

&lt;p&gt;Everything depends on something else. Though you may not have experienced a scenario in which a bootstrapping requirement is currently unsatisfiable, &lt;strong&gt;these scenarios do exist&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Your first line of defense is knowing what things your thing needs in order to start successfully, and protecting that information.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Write everything down
&lt;/h3&gt;

&lt;p&gt;Document, document, document! The earliest parts in this process are the most critical to document. The steps (create an account, save recovery codes, create an API key) seem so simple and obvious, and that's why they're likely to be forgotten.&lt;/p&gt;

&lt;p&gt;In addition, these early steps exist outside of the platform you're building, so it is unlikely that they can be effectively automated.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Define a bootstrap procedure
&lt;/h3&gt;

&lt;p&gt;Define a plan for getting from where you are to where you want to be. You'll likely need to solve the Bootstrapping Problem multiple times to arrive at your end state.&lt;/p&gt;

&lt;p&gt;Here's a hypothetical plan to bootstrap a Kubernetes cluster with Ansible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manually provision an SSH key to a newly installed Debian box. Add this key to secure keyring X and document at location Y.&lt;/li&gt;
&lt;li&gt;Run the Ansible playbook to provision the box as a PXE server. Commit the playbook to Git.&lt;/li&gt;
&lt;li&gt;Create a Debian pre-seed to install the OS and add an SSH key to every box
that joins the network.&lt;/li&gt;
&lt;li&gt;Run the Ansible playbook to provision these boxes as Kubernetes cluster nodes.&lt;/li&gt;
&lt;li&gt;Pass the resulting kubeconfig to Terraform to bootstrap base cluster services.&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi14kt14ttf9mm0vmbt44.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi14kt14ttf9mm0vmbt44.png" alt="Example Kubernetes cluster bootstrap procedure." width="722" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By defining this procedure, we always know the order of dependencies, and we know what layer to consult to recover if the layer above it is broken.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Use a third party
&lt;/h3&gt;

&lt;p&gt;It's okay—stand on the shoulders of giants!&lt;/p&gt;

&lt;p&gt;Even if you plan to host your own GitLab instance, having a few projects on GitLab.com to kickstart your delivery infrastructure is a good idea.&lt;/p&gt;

&lt;p&gt;Build there for a few iterations until you have an self-hosted production environment to run GitLab. Then keep the GitLab.com infrastructure in your back pocket so that you are ready to bootstrap again if you ever need to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Bootstrapping is a universal problem.&lt;/p&gt;

&lt;p&gt;Your work is a result of your earlier work, and other people's work, much like you are a product of your past experiences, the experiences of those around you, and all of human history.&lt;/p&gt;

&lt;p&gt;It really is &lt;a href="https://en.wikipedia.org/wiki/Turtles_all_the_way_down" rel="noopener noreferrer"&gt;turtles all the way down&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In that case, we should strive to know where we came from, how we got to where we are today, and preserve our history to defend our future.&lt;/p&gt;

&lt;p&gt;If you do that much, you'll be able to weather any storm that comes your way, operational or otherwise.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>infrastructure</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Loading config files in Python</title>
      <dc:creator>Brett Weir</dc:creator>
      <pubDate>Mon, 29 May 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/brettops/loading-config-files-in-python-41le</link>
      <guid>https://dev.to/brettops/loading-config-files-in-python-41le</guid>
      <description>&lt;p&gt;Config files are everywhere. There are lots of reasons your app might need to have one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You have configuration that you want to persist beyond a reboot.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your configuration represents a physical state; for example, it contains the settings for peripheral devices, a stored procedure for accomplishing a task, or maybe it expresses the layout of the live user interface.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your app's configuration cannot be easily expressed as a series of variables. CI pipelines, workflows, etc. feature a lot of complex nesting, repeated blocks, and even internal linking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You want the app to be able to persist its own changes to configuration, like changing of windows sizes, menu settings, or credentials. In this case, the config file is functioning more as a database than something the user writes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all of these cases, the structure of the config is very important and likely long-lived. Mistakes in your config syntax will be hard to undo, so it pays to have a plan upfront, and design for it to be extended and documented.&lt;/p&gt;

&lt;p&gt;In this article, we'll learn how to load YAML config files in a way that is clean, easy to support, and easy to extend. We'll do this by creating our own YAML task automation syntax, which we'll call &lt;strong&gt;taskbook&lt;/strong&gt; files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# taskbook.yml&lt;/span&gt;

&lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# name of group&lt;/span&gt;

&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# list of tasks&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# name of task&lt;/span&gt;
    &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# module to use&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# key / value options&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll write a program to read them, which we'll call &lt;strong&gt;Taskable&lt;/strong&gt; *. When finished, it will be easy to determine fields that are supported, validate config values safely, add more fields for future needs, and even access config values within our program as properties.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Any similarity to &lt;a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html" rel="noopener noreferrer"&gt;Ansible playbook syntax&lt;/a&gt;, real or imagined, is purely coincidental.&lt;/em&gt; 😂&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a command line tool
&lt;/h2&gt;

&lt;p&gt;Let's create a file called &lt;code&gt;taskable.py&lt;/code&gt;, to contain our implementation of Taskable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# taskable.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FileType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides the scaffolding for an &lt;code&gt;argparse&lt;/code&gt; command line interface (for more info, see our &lt;a href="https://brettops.io/blog/python-command-line-utilities/" rel="noopener noreferrer"&gt;article on Python CLIs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;You can run the script as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 taskable.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 taskable.py
usage: taskable.py [-h] file
taskable.py: error: the following arguments are required: file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To be able to read in a file, we need to create the file first, which we'll do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a taskbook file
&lt;/h2&gt;

&lt;p&gt;I'll be using YAML for the config files because it's easy to read and I'm comfortable with it, but you can easily support JSON or TOML, as they offer similar APIs.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;taskbook.yml&lt;/code&gt; file and add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# taskbook.yml&lt;/span&gt;
&lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost&lt;/span&gt;
&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;copy file.txt to the place&lt;/span&gt;
    &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;saucy.copy&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;file.txt&lt;/span&gt;
      &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/file.txt&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;install a package&lt;/span&gt;
    &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cheesy.package&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;fzf&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;tree&lt;/span&gt;
      &lt;span class="na"&gt;upgrade&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enable the service&lt;/span&gt;
    &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lettuce.service&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, we'll be able to run the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 taskable.py taskbook.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, nothing will happen because our app doesn't print anything yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read in the YAML file
&lt;/h2&gt;

&lt;p&gt;YAML files are easy to read with Python. There are multiple libraries available, but &lt;a href="https://pyyaml.org/" rel="noopener noreferrer"&gt;&lt;code&gt;pyyaml&lt;/code&gt;&lt;/a&gt; is the de facto standard and is often installed on whatever system you're already on.&lt;/p&gt;

&lt;p&gt;If you don't have &lt;code&gt;pyyaml&lt;/code&gt; (or you're using a virtual environment because you're awesome), install it now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pyyaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your &lt;code&gt;taskable.py&lt;/code&gt; file, import the &lt;code&gt;yaml&lt;/code&gt; package and read in the YAML file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our &lt;code&gt;taskable.py&lt;/code&gt; file so far:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# taskable.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FileType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, you will be able to read in the YAML file, but there's still no output just yet. We could stop here and access its values as nested dictionaries and arrays, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;module&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...but there are a couple problems with this.&lt;/p&gt;

&lt;p&gt;First, there's no validation at all, so a malformed config file has unpredictable results. Second, strings are opaque data, so IDE auto-completion won't work; changing a field name will require manually searching through the code to do so; and I hope you never misspell a field name.&lt;/p&gt;

&lt;p&gt;No, we can do a lot better, and we will, starting by building a model of our data in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the data model
&lt;/h2&gt;

&lt;p&gt;We need a way to express our data format so that it's functional. For this purpose, I prefer to use &lt;a href="https://www.attrs.org/en/stable/index.html" rel="noopener noreferrer"&gt;&lt;code&gt;attrs&lt;/code&gt;&lt;/a&gt;, which gives us data validation, makes our classes more performant, allows us to access our fields as properties with dramatically less boilerplate, and more.&lt;/p&gt;

&lt;p&gt;Let's install &lt;code&gt;attrs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;attrs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add the following to your &lt;code&gt;taskable.py&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;define&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;

&lt;span class="nd"&gt;@define&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@define&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Taskbook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our &lt;code&gt;taskable.py&lt;/code&gt; file so far:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# taskable.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;define&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;

&lt;span class="nd"&gt;@define&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@define&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Taskbook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FileType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two classes—&lt;code&gt;Task&lt;/code&gt; and &lt;code&gt;Taskbook&lt;/code&gt;—fully express the taskbook format. We won't instantiate them ourselves though, because we'll learn a method to do so automagically in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structurize into models
&lt;/h2&gt;

&lt;p&gt;"Structurize" is a $6 word (that I may have made up) that translates to, "load all your data into fancy model classes." I'm using it because "de-serialize" sounds awful and is harder to type. 😝&lt;/p&gt;

&lt;p&gt;The easiest way to structurize your YAML data into &lt;code&gt;attrs&lt;/code&gt; classes is by using the &lt;a href="https://github.com/python-attrs/cattrs" rel="noopener noreferrer"&gt;&lt;code&gt;cattrs&lt;/code&gt;&lt;/a&gt; package. The simplest usage looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cattrs&lt;/span&gt;
&lt;span class="n"&gt;taskbook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cattrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;structure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Taskbook&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's add it to our &lt;code&gt;taskable.py&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# taskable.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cattrs&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;define&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;

&lt;span class="nd"&gt;@define&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@define&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Taskbook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FileType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;taskbook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cattrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;structure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Taskbook&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all you need! &lt;code&gt;cattrs&lt;/code&gt; will load the data into &lt;code&gt;attrs&lt;/code&gt; classes after only being given the expected top-level class, which is &lt;code&gt;Taskbook&lt;/code&gt; here.&lt;/p&gt;

&lt;p&gt;If you need to tweak the behavior, &lt;code&gt;cattrs&lt;/code&gt; provides a &lt;a href="https://catt.rs/en/stable/structuring.html#registering-custom-structuring-hooks" rel="noopener noreferrer"&gt;hook mechanism&lt;/a&gt;. It's a bit cumbersome, but it's easier than writing all the structurization code from scratch.&lt;/p&gt;

&lt;p&gt;In the next section, we'll work on doing something useful with our data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the data
&lt;/h2&gt;

&lt;p&gt;At this point, we've fully structurized our data into classes, which means we can access our config data like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;taskbook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes our code &lt;strong&gt;much&lt;/strong&gt; easier to read and work with. Now we'll try using it to do stuff.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Run" tasks
&lt;/h3&gt;

&lt;p&gt;What good is our script if it can't run tasks? Let's add something to simulate "running" our hypothetical tasks, by adding the following to our &lt;code&gt;taskable.py&lt;/code&gt;file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taskbook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;taskbook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our &lt;code&gt;taskable.py&lt;/code&gt; file so far:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# taskable.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cattrs&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;define&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;

&lt;span class="nd"&gt;@define&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@define&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Taskbook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FileType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;taskbook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cattrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;structure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Taskbook&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taskbook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;taskbook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running our hypothetical tasks will output the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 taskable.py taskbook.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 taskable.py taskbook.yml
group localhost
run saucy.copy: copy file.txt to the place
run cheesy.package: install a package
run lettuce.service: enable the service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not hard to imagine connecting this skeleton to real module implementations to drive real task execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  List used modules
&lt;/h3&gt;

&lt;p&gt;Maybe we'd like to inspect our taskbook to find out what modules it uses. This would be useful, for example, to install necessary modules before running our tasks.&lt;/p&gt;

&lt;p&gt;Let's add a &lt;code&gt;-l&lt;/code&gt; / &lt;code&gt;--list&lt;/code&gt; option to list used modules and exit without running the tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-l&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;store_true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;used_modules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;taskbook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;used_modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our &lt;code&gt;taskable.py&lt;/code&gt; file so far:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# taskable.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cattrs&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;define&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;

&lt;span class="nd"&gt;@define&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@define&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Taskbook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-l&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;store_true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FileType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;taskbook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cattrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;structure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Taskbook&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;used_modules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;taskbook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;used_modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taskbook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;taskbook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;taskable.py&lt;/code&gt; with the list mode enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 taskable.py &lt;span class="nt"&gt;-l&lt;/span&gt; taskbook.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 taskable.py -l taskbook.yaml
cheesy.package
lettuce.service
saucy.copy

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Woot! Static analysis! And it was easy to implement because our data model is so well-defined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we've built up a versatile config loading mechanism.&lt;/p&gt;

&lt;p&gt;This setup works equally well for tiny command line utilities as it does for large and complex data formats files like task workflows, specifications, and so on. You can continue growing your application by adding new fields and new data models, and avoid the malignant technical debt that springs from a muddled early config implementation.&lt;/p&gt;

&lt;p&gt;The best part? Your configuration will be &lt;strong&gt;stable&lt;/strong&gt; and serve the bedrock and foundation of your application, now and in the future. In the words of Eric S. Raymond:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Smart data structures and dumb code works a lot better than the other way around.&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;— Eric S. Raymond&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Stay smart, people! 😄&lt;/p&gt;

</description>
      <category>attrs</category>
      <category>config</category>
      <category>python</category>
      <category>yaml</category>
    </item>
    <item>
      <title>Six reasons to start a business</title>
      <dc:creator>Brett Weir</dc:creator>
      <pubDate>Mon, 22 May 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/brettops/six-reasons-to-start-a-business-3dnl</link>
      <guid>https://dev.to/brettops/six-reasons-to-start-a-business-3dnl</guid>
      <description>&lt;p&gt;There are good and bad reasons to start a business. If you're hoping to get rich quick, never work again, or be the next ZuckerJobs, you might be disappointed. The odds of becoming a billionaire are only somewhat better than &lt;a href="https://www.moneycrashers.com/become-billionaire-characteristics-rich-wealthy/#h-final-word" rel="noopener noreferrer"&gt;being struck by lightning&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;On the other hand, you don't have to spend all your time chasing huge valuations and grand exits. Many people, myself included, start businesses because it makes their lives work better for them.&lt;/p&gt;

&lt;p&gt;In this article, I'll share some reasons why I started my own cloud consulting business, to show that even a modest business can add value to your life, and give you something to talk about forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason #1: Freedom of choice
&lt;/h2&gt;

&lt;p&gt;How much free time do you want? How hard do you want to work? How much money is enough? How often will you get to visit your friends and family?&lt;/p&gt;

&lt;p&gt;Whatever you do in life, there will be tens or hundreds of other things that you &lt;strong&gt;won't&lt;/strong&gt; be able to do. This is our good friend, &lt;strong&gt;opportunity cost&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;As a small business owner, I've found that &lt;strong&gt;I have a choice in how I spend my time&lt;/strong&gt;. I don't ask for permission to take time off, I can plan meetings around my schedule, and no one is looking over my shoulder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason #2: Continuity
&lt;/h2&gt;

&lt;p&gt;Whenever I've left a company in the past, my whole life has been disrupted: the work changes, the tech changes, my insurance changes, and I have to start all over at my new job.&lt;/p&gt;

&lt;p&gt;Starting a business has been a good solution for many of these life disruptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I maintain my own tech stack, tools, and knowledge base, which means I can deliver higher quality work more quickly for new clients.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I have a consistent online presence that doesn't need to be rebuilt all the time, so my network can grow with each new job instead of starting over.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;My insurance doesn't change because it's provided by my own company.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a stable foundation, I spend much less time worrying about retooling and spend more time doing useful work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason #3: Saying "no"
&lt;/h2&gt;

&lt;p&gt;In a corporate setting, there is almost always tension between what you want to build and what they want you to build, but when you're directly employed by an organization, you can't really say "no", or at least not for long.&lt;/p&gt;

&lt;p&gt;When you work for yourself, it's not like that. If your business has any amount of traction, you have the option to say "no" to jobs that don't work for you, are outside your expertise, or are for clients that you just don't feel comfortable with.&lt;/p&gt;

&lt;p&gt;Someone is asking you to violate the laws of physics? Your boss wants you to lie about the fitness of a product for a purpose? If it's your company, you can &lt;strong&gt;just say no&lt;/strong&gt;, allowing you to dodge that bullet and possibly sleep better at night too.&lt;/p&gt;

&lt;p&gt;Remarkably, exercising your right to say "no" can make you &lt;strong&gt;more&lt;/strong&gt; attractive to customers you want to have: ones that deal fairly and treat you like a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason #4: Having a voice
&lt;/h2&gt;

&lt;p&gt;There is a handbook for interacting with customers, writing about what you're doing, or what your organization values. To the extent allowable by law, you get to define all of these relationships, and you get to ask yourself: who am I? What am I building?&lt;/p&gt;

&lt;p&gt;When you have your own small, closely held entity, you are, in large part, the business, and the identity of you and your business are difficult to distinguish (except for tax purposes).&lt;/p&gt;

&lt;p&gt;Starting a business has forced me to communicate more often, to many audiences with different ideas about things. In doing so, I have continued to refine my messaging, understand my own identity, and put on paper what my values actually are. It is forever a work in progress, but it's a task that I did not expect would shape my life in the way that it has.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason #5: Knowing yourself
&lt;/h2&gt;

&lt;p&gt;Having a business, your own business, forces you to think about what's truly important to you.&lt;/p&gt;

&lt;p&gt;What's important enough that would be worth taking a massive pay cut? Or being stressed out? Or needing to learn a bunch about tax code? I personally know people who &lt;strong&gt;have&lt;/strong&gt; tried to have their own business, and in the process, realized that, ya know, maybe having their own business isn't for them and that the working world wasn't so bad.&lt;/p&gt;

&lt;p&gt;If you're looking for powerful self-reflection, starting a business provides lots of opportunities for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason #6: Learning
&lt;/h2&gt;

&lt;p&gt;When you start a business, your job on any given day is always changing. Whether it's code-slinging, hardware, documentation, bookkeeping, marketing, filming, editing, paperwork; there is always something more to do, and often it's something you've never done before.&lt;/p&gt;

&lt;p&gt;I've learned a lot from regular jobs, but I'd wager that every one year I've worked as a solo founder has been worth ten plugging away at a job somewhere. It's not enough to do your little part at a startup. You have to know everything, you have to understand everything, and you have to be able to communicate everything to everyone.&lt;/p&gt;

&lt;p&gt;On top of that, every second counts. Every conversation, every email, every line of code counts, and there is a visceral connection between the work you do and what value it does or doesn't bring to the organization.&lt;/p&gt;

&lt;p&gt;You just don't get that kind of feedback from a performance review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You don't have to sell your soul to live a good life. &lt;strong&gt;It's okay to be small&lt;/strong&gt;, and there are lots of ways to go about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Becoming a consultant&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Building a tiny SaaS product&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Franchising a business&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Selling things online&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's different for everyone, and it won't be easy; I'm not getting rich any time soon with BrettOps. But I like to set my own hours, I don't like asking for permission, and I like having an interesting story to tell at the end of the day.&lt;/p&gt;

&lt;p&gt;If any of that sounds like you, then starting your own small business might be just what the doctor ordered.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If my reasons for starting BrettOps align with yours, you can &lt;a href="mailto:brett@brettops.io"&gt;reach out&lt;/a&gt;. I love meeting new people.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>business</category>
      <category>career</category>
      <category>life</category>
    </item>
    <item>
      <title>What is Terraform?</title>
      <dc:creator>Brett Weir</dc:creator>
      <pubDate>Mon, 15 May 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/brettops/what-is-terraform-1c25</link>
      <guid>https://dev.to/brettops/what-is-terraform-1c25</guid>
      <description>&lt;p&gt;HashiCorp's &lt;a href="https://www.terraform.io/" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; is a powerful tool that enables higher-order infrastructure management across tools and providers. It does this by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Providing a standardized, declarative front end for remote APIs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Supporting gradual change, rollback, and disaster recovery&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Making your infrastructure self-documenting&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Supporting immutable infrastructure deployment patterns&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That all sounds great, but what does any of that mean? To understand Terraform, I find it best to understand what it replaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about ...
&lt;/h2&gt;

&lt;p&gt;Let's say you have a server somewhere. You need to put stuff on it. How should you approach it?&lt;/p&gt;

&lt;h3&gt;
  
  
  ... hand edits?
&lt;/h3&gt;

&lt;p&gt;The easiest possible solution is to hack away. They have a UI for a reason, right? Log right in, tweak the config, install some things, add your SSH keys, and away you go. This will work famously, and potentially for a very long time. That is, of course, until it doesn't.&lt;/p&gt;

&lt;p&gt;Cracks will start to show sooner or later. Someone will ask you to change something. Your app will get more users. You'll need to migrate something to somewhere. You'll hand off some work to the new team member. The server's OS reaches end-of-life. Over time, these miscellaneous changes accumulate and are then forgotten as the team moves on to new work. This is called &lt;strong&gt;configuration drift&lt;/strong&gt; , and it is ruinous because it is both catastrophic and inevitable. Any server that sits there long enough will eventually succumb to this fate.&lt;/p&gt;

&lt;h3&gt;
  
  
  ... change orders?
&lt;/h3&gt;

&lt;p&gt;One solution is to neurotically control and gate all changes to systems, requiring operators to fill out paperwork and get changes approved and signed off by multiple people.&lt;/p&gt;

&lt;p&gt;This "helps" to prevent changes by demoralizing the team and making them resistant to working on or improving the system at all. If, despite that, someone does eventually try to make a change, then it does little to prevent erroneous or incidental changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  ... scripts?
&lt;/h3&gt;

&lt;p&gt;Another solution is writing a script to do the deployment. This is a stunning advancement in practice, as it acknowledges that making changes yourself is probably a bad idea. It also helps you remember how you did something, why, and lets you do it again if needed.&lt;/p&gt;

&lt;p&gt;A problem you quickly discover with this approach is the pleasure of coding for all possible circumstances. Writing a script to install a package? Okay, great. What should I do if the package is already installed? Should I install the newer version? Remove and reinstall? Purge existing configuration associated with the package? What if there's a conflicting package?&lt;/p&gt;

&lt;p&gt;Coding for every eventuality on a long-lived system is nearly impossible.&lt;/p&gt;

&lt;p&gt;Another problem is that, you're still running your script on a live, existing server, which means your install script isn't the only thing potentially making changes. Your system auto updater is also making changes, and so are the support techies who are panic fixing the bug that got introduced. By extension, there's no great way to roll back your changes, because there is no well-defined answer to knowing what changes have actually been made. Sure, your can restore the image from a backup, but what exactly does that do (or undo)?&lt;/p&gt;

&lt;h3&gt;
  
  
  ... configuration management?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Software_configuration_management" rel="noopener noreferrer"&gt;Configuration management (CM) tools&lt;/a&gt; like Chef, Puppet, and Ansible have their uses. In many ways, the benefits and drawbacks of CM tools are the same as for writing scripts.&lt;/p&gt;

&lt;p&gt;They bring extra features to the table to support managing multiple machines, and come equipped with packages and modules to make coding &lt;a href="https://en.wikipedia.org/wiki/Idempotence" rel="noopener noreferrer"&gt;idempotent&lt;/a&gt; changes easier. They still suffer from incomplete knowledge of system state, and you still gotta code for the great many situations that you may run into while provisioning.&lt;/p&gt;

&lt;p&gt;So how &lt;strong&gt;do&lt;/strong&gt; you manage a server over its lifetime?&lt;/p&gt;

&lt;p&gt;It's a trick question: &lt;strong&gt;you don't&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Terraform
&lt;/h2&gt;

&lt;p&gt;Terraform is a different kind of tool entirely. It's a configuration management tool, but it approaches things so differently that it's really in a class of its own.&lt;/p&gt;

&lt;p&gt;Terraform abandons the idea that things should be long-lived at all. It treats things it manages as immutable black boxes. You might be able to change a label or a tag, but for any change that might change the nature of a resource, like installing package or changing certain parameters, Terraform prefers to just delete it and start over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What?!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a big departure from endlessly tweaking systems forever. Now, instead of operators getting mired in a sea of patches and updates and configs, resources become atomic building blocks that can be assembled and reassembled as needed.&lt;/p&gt;

&lt;p&gt;To demonstrate how Terraform works, we'll deploy a DigitalOcean droplet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project setup
&lt;/h3&gt;

&lt;p&gt;Terraform projects are written in &lt;a href="https://developer.hashicorp.com/terraform/language" rel="noopener noreferrer"&gt;their own configuration language&lt;/a&gt;, and Terraform files end in &lt;code&gt;.tf&lt;/code&gt;. Terraform syntax is declarative, so ordering doesn't matter, and resources are defined at a directory, or &lt;a href="https://developer.hashicorp.com/terraform/language/modules" rel="noopener noreferrer"&gt;"module"&lt;/a&gt;, level, so you can spread your resources out across multiple &lt;code&gt;.tf&lt;/code&gt; files in a directory, and name them whatever you like.&lt;/p&gt;

&lt;p&gt;Create a directory called whatever you like, and add &lt;code&gt;main.tf&lt;/code&gt; and&lt;code&gt;terraform.tf&lt;/code&gt; files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;droplet-deployment/
&lt;span class="nb"&gt;cd &lt;/span&gt;droplet-deployment/
&lt;span class="nb"&gt;touch &lt;/span&gt;main.tf terraform.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Finding providers
&lt;/h3&gt;

&lt;p&gt;The next thing to do is find a provider.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://developer.hashicorp.com/terraform/language/providers" rel="noopener noreferrer"&gt;provider&lt;/a&gt; is a plugin that wraps an API so that it can be modeled declaratively using HCL code. Companies, organizations, and individuals maintain Terraform providers because they understand the value that Terraform provides, and know that many developers will refuse to integrate services that don't offer Terraform providers. 😄&lt;/p&gt;

&lt;p&gt;We can search the &lt;a href="https://registry.terraform.io/" rel="noopener noreferrer"&gt;Terraform Registry&lt;/a&gt;, where we find the &lt;a href="https://registry.terraform.io/providers/digitalocean/digitalocean/latest" rel="noopener noreferrer"&gt;&lt;code&gt;digitalocean/digitalocean&lt;/code&gt;provider&lt;/a&gt;, maintained by none other than DigitalOcean themselves. Let's add it to the &lt;code&gt;terraform.tf&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# terraform.tf&lt;/span&gt;
&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;digitalocean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean/digitalocean"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 2.28"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can add a credential in whatever way desired to connect to our DigitalOcean account. I prefer environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DIGITALOCEAN_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"dop_v1_XXXXXXXXXXXXXXXX"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;terraform init&lt;/code&gt; to download the providers and configure your state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;terraform init
&lt;span class="go"&gt;
Initializing the backend...

Initializing provider plugins...
&lt;/span&gt;&lt;span class="gp"&gt;- Finding digitalocean/digitalocean versions matching "~&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;2.28&lt;span class="s2"&gt;"...
&lt;/span&gt;&lt;span class="go"&gt;- Installing digitalocean/digitalocean v2.28.1...
- Installed digitalocean/digitalocean v2.28.1 (signed by a HashiCorp partner, key ID F82037E524B9C0E8)

&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;Terraform has been successfully initialized!
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can then declare our droplet resource &lt;a href="https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/droplet" rel="noopener noreferrer"&gt;according to the example&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.tf&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean_droplet"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ubuntu-22-04-x64"&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sfo3"&lt;/span&gt;
  &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s-1vcpu-1gb"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We haven't created anything yet though. That comes next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making plans
&lt;/h3&gt;

&lt;p&gt;Terraform has an excellent view of the systems that it manages. It knows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;actual&lt;/strong&gt; state of the system, by querying the target API,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;expected&lt;/strong&gt; state of the system, by reading the state file, and&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;desired&lt;/strong&gt; state of the system, by reading the HCL files in your repository.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Through all these views of the system, Terraform is able to build a &lt;a href="https://en.wikipedia.org/wiki/Directed_graph" rel="noopener noreferrer"&gt;directed graph&lt;/a&gt; of changes required to achieve the desired state. This is known, in Terraform jargon, as a "plan".&lt;/p&gt;

&lt;p&gt;After writing out your HCL code, you can create a plan using the &lt;code&gt;terraform plan&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On running the plan, Terraform will tell you exactly what changes it intends to make, so you can review them before Terraform does anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;terraform plan
&lt;span class="go"&gt;
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

&lt;/span&gt;&lt;span class="gp"&gt;  #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;digitalocean_droplet.app will be created
&lt;span class="go"&gt;  + resource "digitalocean_droplet" "app" {
      + backups = false
      + created_at = (known after apply)
      + disk = (known after apply)
      + graceful_shutdown = false
      + id = (known after apply)
      + image = "ubuntu-22-04-x64"
      + ipv4_address = (known after apply)
      + ipv4_address_private = (known after apply)
      + ipv6 = false
      + ipv6_address = (known after apply)
      + locked = (known after apply)
      + memory = (known after apply)
      + monitoring = false
      + name = "app"
      + price_hourly = (known after apply)
      + price_monthly = (known after apply)
      + private_networking = (known after apply)
      + region = "sfo3"
      + resize_disk = true
      + size = "s-1vcpu-1gb"
      + status = (known after apply)
      + urn = (known after apply)
      + vcpus = (known after apply)
      + volume_ids = (known after apply)
      + vpc_uuid = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run
"terraform apply" now.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This is huge&lt;/strong&gt;. Terraform allows you to see and understand the changes you're about to make &lt;strong&gt;before&lt;/strong&gt; you make them. You know, so you don't do fun things like clobber the database or delete the firewall.&lt;/p&gt;

&lt;p&gt;You can visualize your plan with the &lt;code&gt;graph&lt;/code&gt; subcommand, which renders Graphviz code to render into an image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform graph | dot &lt;span class="nt"&gt;-Tsvg&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; plan.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.brettops.io%2Fblog%2Fwhat-is-terraform%2Fplan.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.brettops.io%2Fblog%2Fwhat-is-terraform%2Fplan.svg" alt="A Graphviz rendering of our desired Terraform state." width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, we see that Terraform will instantiate the DigitalOcean provider and use that to create our droplet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Applying plans
&lt;/h3&gt;

&lt;p&gt;When you are feeling confident in the changes you're about to make, you can "apply" them, which executes your plan's proposed changes to arrive at the new state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;terraform apply
&lt;span class="c"&gt;...
...
...
&lt;/span&gt;&lt;span class="go"&gt;Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

digitalocean_droplet.app: Creating...
digitalocean_droplet.app: Still creating... [10s elapsed]
digitalocean_droplet.app: Still creating... [20s elapsed]
digitalocean_droplet.app: Still creating... [30s elapsed]
digitalocean_droplet.app: Still creating... [40s elapsed]
digitalocean_droplet.app: Creation complete after 42s [id=355390759]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we browse to the DigitalOcean dashboard, the resource is here, it's live, it's ready to go!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgxw99asa9bn75gek4cd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgxw99asa9bn75gek4cd.png" alt="The droplet list page in DigitalOcean." width="800" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsqtpfa5xpsx2i8gont4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsqtpfa5xpsx2i8gont4e.png" alt="The detail page for our droplet." width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Well, not quite ready. I mean, there's no SSH key, no firewall, no monitoring. We'll look at that in the next section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making changes
&lt;/h3&gt;

&lt;p&gt;Once you deploy a machine, the first thing you'll want to do is make changes to it, I guarantee you. And isn't that why we're here? 😄&lt;/p&gt;

&lt;p&gt;Here's a fairly obvious and immediate change you'll want to make to this image. It has no SSH key, so there's currently no way at all to access it.&lt;/p&gt;

&lt;p&gt;Normally, you might bust out &lt;code&gt;ssh-keygen&lt;/code&gt; and have at it, but then you're back where you started, making hand-edits to things with no way to track it. Instead, add the &lt;a href="https://registry.terraform.io/providers/hashicorp/tls/latest" rel="noopener noreferrer"&gt;&lt;code&gt;hashicorp/tls&lt;/code&gt;provider&lt;/a&gt; to generate SSH keys from your Terraform configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# terraform.tf&lt;/span&gt;
&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;digitalocean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean/digitalocean"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 2.28"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;tls&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/tls"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"4.0.4"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since you added a new provider, you'll need to run &lt;code&gt;terraform init -upgrade&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init &lt;span class="nt"&gt;-upgrade&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;terraform init &lt;span class="nt"&gt;-upgrade&lt;/span&gt;
&lt;span class="go"&gt;
Initializing the backend...

Initializing provider plugins...
&lt;/span&gt;&lt;span class="gp"&gt;- Finding digitalocean/digitalocean versions matching "~&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;2.28&lt;span class="s2"&gt;"...
&lt;/span&gt;&lt;span class="go"&gt;- Finding hashicorp/tls versions matching "4.0.4"...
- Using previously-installed digitalocean/digitalocean v2.28.1
- Installing hashicorp/tls v4.0.4...
- Installed hashicorp/tls v4.0.4 (signed by HashiCorp)

Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

Terraform has been successfully initialized!
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you'll need to add two resources: one to &lt;a href="https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key" rel="noopener noreferrer"&gt;create the key itself&lt;/a&gt;, and another to &lt;a href="https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/ssh_key" rel="noopener noreferrer"&gt;add the key to DigitalOcean&lt;/a&gt;. Then modify the droplet resource to add the key to the machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.tf&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"tls_private_key"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;algorithm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ED25519"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean_ssh_key"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app ssh key"&lt;/span&gt;
  &lt;span class="nx"&gt;public_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tls_private_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_key_openssh&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean_droplet"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ubuntu-22-04-x64"&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sfo3"&lt;/span&gt;
  &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s-1vcpu-1gb"&lt;/span&gt;

  &lt;span class="nx"&gt;ssh_keys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;digitalocean_ssh_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, if we run &lt;code&gt;graph&lt;/code&gt;, we should see this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.brettops.io%2Fblog%2Fwhat-is-terraform%2Fplan2.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.brettops.io%2Fblog%2Fwhat-is-terraform%2Fplan2.svg" alt="The predicted state of our infrastructure after adding the new resources." width="1210" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's not required to run &lt;code&gt;plan&lt;/code&gt; before &lt;code&gt;apply&lt;/code&gt;, because Terraform will automatically create a plan anyway if it needs to. Let's just run &lt;code&gt;terraform apply&lt;/code&gt;, and examine closely what's going on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;terraform apply
&lt;span class="go"&gt;digitalocean_droplet.app: Refreshing state... [id=355390759]
&lt;/span&gt;&lt;span class="c"&gt;...
...
...
&lt;/span&gt;&lt;span class="go"&gt;Plan: 3 to add, 0 to change, 1 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

digitalocean_droplet.app: Destroying... [id=355390759]
digitalocean_droplet.app: Still destroying... [id=355390759, 10s elapsed]
digitalocean_droplet.app: Still destroying... [id=355390759, 20s elapsed]
digitalocean_droplet.app: Destruction complete after 21s
tls_private_key.app: Creating...
tls_private_key.app: Creation complete after 0s [id=d680d387c543c08d6d015ad893164bf97a6bf495]
digitalocean_ssh_key.app: Creating...
digitalocean_ssh_key.app: Creation complete after 1s [id=38310647]
digitalocean_droplet.app: Creating...
digitalocean_droplet.app: Still creating... [10s elapsed]
digitalocean_droplet.app: Still creating... [20s elapsed]
digitalocean_droplet.app: Still creating... [30s elapsed]
digitalocean_droplet.app: Still creating... [40s elapsed]
digitalocean_droplet.app: Creation complete after 42s [id=355414489]

Apply complete! Resources: 3 added, 0 changed, 1 destroyed.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You and I have added two resources, and modified the existing one, but Terraform is reporting that we're adding three and &lt;strong&gt;destroying&lt;/strong&gt; one. 😲&lt;/p&gt;

&lt;p&gt;This is because the droplet resource will &lt;strong&gt;not&lt;/strong&gt; attempt to modify an existing virtual machine, for all the reasons we've discussed. Instead, it will delete the instance and create a new one from scratch. This means that our machine is fresh, completely blank, and unmodified.&lt;/p&gt;

&lt;p&gt;This could be frustrating if we had saved a bunch of stuff on that machine. If, instead of doing that, we just accept that our whole machine will be destroyed periodically, then we have the closest thing in existence to a precisely known machine image, because &lt;strong&gt;we fully record the steps to create the machine&lt;/strong&gt; , and execute those steps each time to create it.&lt;/p&gt;

&lt;p&gt;If we extend this idea to our infrastructure, we can do things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Rotate the SSH keys on our boxes any time we like&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use a different SSH key for every box&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Swap the OS image or modify our runtime environment whenever we need to&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives us a lot of freedom to change things that we didn't previously have.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleaning up
&lt;/h3&gt;

&lt;p&gt;Imagine yourself, messing around with dozens of clusters. You're so cool, you're spinning them up like nobody's business, trying out GPUs and storage drivers and all kinds of fun stuff. Only problem is, they're expensive! How do you clean up after yourself in a sane way?&lt;/p&gt;

&lt;p&gt;With every other tool in existence, &lt;strong&gt;you get to clean it up yourself&lt;/strong&gt;!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If you used Ansible, you get to write a destroy playbook.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you wrote a script, you're now writing a destroy script.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you did it by hand through the web UI, here are the keys to the building and make sure lock up when you're done.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Terraform, on the other hand, already knows what exists and how it would delete it. All it needs is to be told that you no longer want your resources anymore, with the &lt;code&gt;destroy&lt;/code&gt; subcommand:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; This will delete all your stuff!&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;terraform destroy
&lt;span class="go"&gt;tls_private_key.app: Refreshing state... [id=d680d387c543c08d6d015ad893164bf97a6bf495]
digitalocean_ssh_key.app: Refreshing state... [id=38310647]
digitalocean_droplet.app: Refreshing state... [id=355414489]
&lt;/span&gt;&lt;span class="c"&gt;...
...
...
&lt;/span&gt;&lt;span class="go"&gt;Plan: 0 to add, 0 to change, 3 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

digitalocean_droplet.app: Destroying... [id=355414489]
digitalocean_droplet.app: Still destroying... [id=355414489, 10s elapsed]
digitalocean_droplet.app: Still destroying... [id=355414489, 20s elapsed]
digitalocean_droplet.app: Destruction complete after 21s
digitalocean_ssh_key.app: Destroying... [id=38310647]
digitalocean_ssh_key.app: Destruction complete after 1s
tls_private_key.app: Destroying... [id=d680d387c543c08d6d015ad893164bf97a6bf495]
tls_private_key.app: Destruction complete after 0s

Destroy complete! Resources: 3 destroyed.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it! All of your resources are gone, and you don't need to wonder if you're going to get billed next month for things you forgot to delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Because every resource is created via a declarative specification &lt;strong&gt;that is committed to version control&lt;/strong&gt; , Terraform deployments are inherently self-documenting. There is no question as to what you deployed, when, and how.&lt;/p&gt;

&lt;p&gt;Because Terraform knows how to resolve differences between states, there is also no question about what changes would be required to move from one state to another, and there is always a path to do so, via Terraform.&lt;/p&gt;

&lt;p&gt;It's difficult to overstate how valuable this is. Terraform makes past, present, and future infrastructure states representable and gives you a complete, executable view of your systems, allowing you to understand, modify, and even reproduce systems in exquisite detail.&lt;/p&gt;

&lt;p&gt;I can't imagine working on infrastructure without Terraform. It enables my work in a way that no other tool has up to this point, and it is the glue that holds all the rest of my tools, projects, and projects together.&lt;/p&gt;

&lt;p&gt;If you haven't tried Terraform, I highly recommend it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Download the completed example project:&lt;br&gt;&lt;strong&gt;&lt;a href="https://gitlab.com/brettops/examples/droplet-deployment" rel="noopener noreferrer"&gt;https://gitlab.com/brettops/examples/droplet-deployment&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>cloud</category>
      <category>deployment</category>
      <category>devops</category>
      <category>terraform</category>
    </item>
  </channel>
</rss>
