<?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: Friedrich Kurz</title>
    <description>The latest articles on DEV Community by Friedrich Kurz (@fkurz).</description>
    <link>https://dev.to/fkurz</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%2Fuser%2Fprofile_image%2F866763%2F64c542dd-be36-49d1-b9eb-dbf5d4d5f80c.png</url>
      <title>DEV Community: Friedrich Kurz</title>
      <link>https://dev.to/fkurz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fkurz"/>
    <language>en</language>
    <item>
      <title>Extending AstroJS Markdown Processing With Remark and Rehype Plugins</title>
      <dc:creator>Friedrich Kurz</dc:creator>
      <pubDate>Sat, 17 Aug 2024 10:03:08 +0000</pubDate>
      <link>https://dev.to/fkurz/extending-astrojs-markdown-processing-with-remark-and-rehype-plugins-m1k</link>
      <guid>https://dev.to/fkurz/extending-astrojs-markdown-processing-with-remark-and-rehype-plugins-m1k</guid>
      <description>&lt;h2&gt;
  
  
  AstroJS
&lt;/h2&gt;

&lt;p&gt;AstroJS is a framework for static website building that has a lot of exciting features like&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reducing &lt;strong&gt;page size&lt;/strong&gt; (and therefore faster loading speed);&lt;/li&gt;
&lt;li&gt;being &lt;strong&gt;framework agnostic&lt;/strong&gt;  (React, Svelte, what have you are all supported);&lt;/li&gt;
&lt;li&gt;lowering &lt;strong&gt;client-side scripting&lt;/strong&gt; (a lot of processing is done on the server and AstroJS ships pure HTML by default);&lt;/li&gt;
&lt;li&gt;being &lt;strong&gt;component-based&lt;/strong&gt; and supporting &lt;strong&gt;component de-hydration&lt;/strong&gt; (components are loaded lazily when needed);&lt;/li&gt;
&lt;li&gt;support for &lt;strong&gt;MDX&lt;/strong&gt; (Markdown + JSX), &lt;strong&gt;TypeScript&lt;/strong&gt;, &lt;strong&gt;CSS Pre-Processing&lt;/strong&gt;, and &lt;strong&gt;CSS frameworks&lt;/strong&gt; (like Tailwind CSS); and &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;integrated SEO automations&lt;/strong&gt; (like automatic sitemap generation, RSS feeds, and pagination)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;🔗 &lt;a href="https://astro.build/blog/introducing-astro/" rel="noopener noreferrer"&gt;(astro.build) Introducing Astro&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another great aspect of AstroJS is that it supports extensions of Markdown processing via &lt;strong&gt;Remark&lt;/strong&gt; and &lt;strong&gt;Rehype&lt;/strong&gt; plugins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remark and Rehype
&lt;/h2&gt;

&lt;p&gt;Both &lt;a href="https://github.com/remarkjs/remark" rel="noopener noreferrer"&gt;Remark&lt;/a&gt; and &lt;a href="https://github.com/rehypejs/rehype" rel="noopener noreferrer"&gt;Rehype&lt;/a&gt; are frameworks to transform documents by first transforming them to &lt;strong&gt;abstract syntax trees&lt;/strong&gt; (AST) and then applying pluggable functions to the AST before converting it to the target format.&lt;/p&gt;

&lt;p&gt;Remark parses Markdown and MDX files and applies transformations. Rehype parses HTML and applies transformations. Both frameworks have a large ecosystem of community plugins available.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Remark and Rehype may be combined if we export HTML after transformation with Remark, for example by using the &lt;a href="https://github.com/remarkjs/remark-rehype" rel="noopener noreferrer"&gt;&lt;code&gt;remark-rehype&lt;/code&gt; plugin&lt;/a&gt;. AstroJS automatically does this for use, so we do not need to worry about the transformation and may directly write plugins for Remark and Rehype.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Extending AstroJS With Remark and Rehype
&lt;/h2&gt;

&lt;p&gt;A Remark or Rehype plugin is a higher-order function—a function returning another function in this case—that takes an &lt;code&gt;options&lt;/code&gt; argument for configuration and then returns a function that receives the abstract syntax tree (AST) of the parsed Markdown or HTML document.&lt;/p&gt;

&lt;p&gt;The abstract syntax tree for both tools looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'root'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;children:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;children:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;position:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;position:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;start:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;line:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;column:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;offset:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;end:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;line:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;column:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;offset:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1201&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plugins for both Remark and Rehype may be registered in the &lt;a href="https://docs.astro.build/en/guides/markdown-content/" rel="noopener noreferrer"&gt;Markdown or MDX integrations&lt;/a&gt;  in &lt;code&gt;astro.config.mjs&lt;/code&gt;. Below, is an example configuration of the MDX integration with a Remark plugin &lt;code&gt;theRemarkPlugin&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;astro/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;theRemarkPlugin&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/remark/the-remark-plugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// https://astro.build/config&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;mdx&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;remarkPlugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;theRemarkPlugin&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;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 If we want to add a Remark or Rehype plugin to the Markdown processor, we have to add it to &lt;code&gt;markdown.remarkPlugins&lt;/code&gt; respectively &lt;code&gt;markdown.rehypePlugins&lt;/code&gt; of the configuration object passed to &lt;code&gt;defineConfig&lt;/code&gt; instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Example: Enabling &lt;code&gt;rehype-mermaid&lt;/code&gt; in AstroJS
&lt;/h2&gt;

&lt;p&gt;Lets, assume we want to add rendered &lt;a href="https://mermaid.js.org" rel="noopener noreferrer"&gt;Mermaid&lt;/a&gt; diagrams to our page by transforming the source code in fenced code blocks like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;mermaid
&lt;/span&gt;&lt;span class="sb"&gt;flowchart LR

A[Hard] --&amp;gt;|Text| B(Round)
B --&amp;gt; C{Decision}
C --&amp;gt;|One| D[Result 1]
C --&amp;gt;|Two| E[Result 2]&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thankfully, a Rehype plugin to generate Mermaid diagrams &lt;code&gt;rehype-mermaid&lt;/code&gt; from HTML &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt;or &lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt; elements with Mermaid diagram source code already exists. Adding it to our Markdown or MDX processing pipeline is therefore as easy as installing it with NPM and adding it to the configuration as shown above.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔗 &lt;a href="https://github.com/remcohaszing/rehype-mermaid" rel="noopener noreferrer"&gt;(github.com) Rehype Mermaid Plugin&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://github.com/remcohaszing/rehype-mermaid" rel="noopener noreferrer"&gt;plugin's documentation&lt;/a&gt;, however, states that it only converts &lt;code&gt;&amp;lt;pre class="mermaid"&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;code class="language-mermaid"&amp;gt;&lt;/code&gt; elements to rendered diagrams in SVG format. &lt;/p&gt;

&lt;p&gt;And, unfortunately, if we add a fenced code block with language &lt;code&gt;mermaid&lt;/code&gt; to our post, the element produced by remark is a &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; element with a class list like &lt;code&gt;astro-code github-dark&lt;/code&gt;. To get our rendered Mermaid diagram, we have to add the class &lt;code&gt;mermaid&lt;/code&gt; to this element.&lt;/p&gt;

&lt;p&gt;With the tooling provided for Rehype via projects in the &lt;a href="https://github.com/syntax-tree/unist" rel="noopener noreferrer"&gt;&lt;code&gt;unist&lt;/code&gt;&lt;/a&gt; ecosystem, this is luckily rather straight forward. To help finding the nodes of the AST that we are concerned with and transforming them as need, we may use the &lt;code&gt;visit&lt;/code&gt; function from &lt;a href="https://github.com/flex-development/unist-util-visit" rel="noopener noreferrer"&gt;&lt;code&gt;unist-util-visit&lt;/code&gt;&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;This function, may—for simple scenarios like ours—be defined with only two parameters&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ast&lt;/code&gt; (the AST's root) and &lt;/li&gt;
&lt;li&gt;a &lt;code&gt;visitor&lt;/code&gt; function that applies our changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Omitting some implementation details, our Rehype plugin &lt;code&gt;addMermaidClass&lt;/code&gt; to add the &lt;code&gt;mermaid&lt;/code&gt; class to Mermaid code blocks then may be written as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CONTINUE&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unist-util-visit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Plugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unified&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Element&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/* ... */&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addMermaidClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Plugin&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;Root&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;visitor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;addMermaidClass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's now take a look at the implementation of the &lt;code&gt;visitor&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dataLanguageMermaid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mermaid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;typeElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;element&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tagNamePre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pre&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;classMermaid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dataLanguageMermaid&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isPreElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;typeElement&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;tagNamePre&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataLanguage&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;dataLanguageMermaid&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isPreElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;CONTINUE&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Element&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;classMermaid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;CONTINUE&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important things to note are &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the first conditional which assures that we are visiting the AST representation of a &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; element and skips all other elements and&lt;/li&gt;
&lt;li&gt;the in-place modification of the &lt;code&gt;node.properties.className&lt;/code&gt; array.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Note that the &lt;code&gt;visit&lt;/code&gt; function should return an object of type &lt;code&gt;Action&lt;/code&gt; instead of the visited node. In this case, we return the &lt;a href="https://github.com/flex-development/unist-util-visit?tab=readme-ov-file#continue" rel="noopener noreferrer"&gt;&lt;code&gt;CONTINUE&lt;/code&gt;&lt;/a&gt; action that tells Remark to continue traversing the AST.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The last step is to import the plugin and add it to the configuration of the MDX integration &lt;strong&gt;before&lt;/strong&gt; the &lt;code&gt;rehypeMermaid&lt;/code&gt; plugin. My AstroJS at the time of writing looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;astro/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;astro-icon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;tailwind&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@astrojs/tailwind&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mdx&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@astrojs/mdx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;rehypeMermaid&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rehype-mermaid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;addMermaidClass&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/rehype/add-mermaid-class&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// https://astro.build/config&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;tailwind&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;mdx&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;rehypePlugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;addMermaidClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rehypeMermaid&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;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 Adding the &lt;code&gt;addMermaidClass&lt;/code&gt; plugin before the &lt;code&gt;rehypeMermaid&lt;/code&gt; plugin is important since the array order defines the execution order of the plugins and we need to change the class name before passing the AST on to the &lt;code&gt;rehypeMermaid&lt;/code&gt; plugin. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rendered result of the Mermaid diagram is shown below:&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%2Fzo6upe8402s9imk3cit0.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%2Fzo6upe8402s9imk3cit0.png" alt="Rendered Mermaid Diagram" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More on AstroJS
&lt;/h2&gt;

&lt;p&gt;Check out my blog for more posts about AstroJS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2025-01-11/" rel="noopener noreferrer"&gt;Static Page Redirects using AstroJS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.friedrichkurz.me/posts/2024-09-13/]" rel="noopener noreferrer"&gt;Adding Automatic Sitemap Generation to an AstroJS Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-08-09/" rel="noopener noreferrer"&gt;Markdown Plugins for AstroJS: Remark and Rehype&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-08-07/" rel="noopener noreferrer"&gt;Adding Latex Rendering to AstroJS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-08-04/" rel="noopener noreferrer"&gt;Filtering Elements in AstroJS Content Collections&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-06-23/" rel="noopener noreferrer"&gt;Key features of Astro JS for static website building&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>astro</category>
      <category>remark</category>
      <category>rehype</category>
      <category>markdown</category>
    </item>
    <item>
      <title>How to define and work with a Rust-like result type in NuShell</title>
      <dc:creator>Friedrich Kurz</dc:creator>
      <pubDate>Fri, 21 Jun 2024 11:51:00 +0000</pubDate>
      <link>https://dev.to/fkurz/how-to-define-and-work-with-a-rust-like-result-type-in-nushell-51ib</link>
      <guid>https://dev.to/fkurz/how-to-define-and-work-with-a-rust-like-result-type-in-nushell-51ib</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;Rust—among other modern programming languages—has a data type &lt;a href="https://doc.rust-lang.org/std/result/" rel="noopener noreferrer"&gt;&lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt; in its standard library&lt;/a&gt;, that allows us to represent error states in a program directly in code. Using data structures to represent errors in code is a pattern known mostly from purely functional programming languages but has gained some traction in programming languages that follow a less restrictive language paradigm (like Rust). &lt;/p&gt;

&lt;p&gt;The idea of modelling errors in code is that—following the functional credo—everything the function does should be contained in the return value. Side effects, like errors, should be avoided wherever possible. &lt;/p&gt;

&lt;p&gt;This is a nice pattern, since it forces you to address that code may not succeed. (If you don't ignore the return value, of course. :D) Combining the use of &lt;code&gt;Result&lt;/code&gt; return types with &lt;a href="https://doc.rust-lang.org/book/ch18-00-patterns.html" rel="noopener noreferrer"&gt;Rust's pattern matching&lt;/a&gt; also improves code legibility, in my opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error handling in NuShell
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://nushell.sh" rel="noopener noreferrer"&gt;NuShell&lt;/a&gt; is a very modern shell and shell language that draws some heavy inspiration from Rust and that I really enjoy writing small programs and glue code in (especially for CI/CD pipelines). &lt;/p&gt;

&lt;p&gt;In contrast to Rust, NuShell has a &lt;a href="https://www.nushell.sh/commands/docs/try.html" rel="noopener noreferrer"&gt;&lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt;&lt;/a&gt; control structure to capture and deal with errors. There is no result type in the &lt;a href="https://github.com/nushell/nushell/tree/main/crates/nu-std" rel="noopener noreferrer"&gt;standard library&lt;/a&gt; at the time of writing. &lt;/p&gt;

&lt;p&gt;NuShell's &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt;, moreover, has the major downside, that you cannot react to specifics of an error, since the &lt;code&gt;catch&lt;/code&gt; block doesn't receive any parameter like an exception object, that would allow us introspection on what went wrong. &lt;/p&gt;

&lt;p&gt;So what can we do? Well, we may just define a Result type ourselves and use it. Since NuShell also has pattern matching using the &lt;code&gt;match&lt;/code&gt; keyword, we can write some pretty readable code with it.&lt;/p&gt;

&lt;p&gt;Consider, for example, malformed URLs when using the &lt;code&gt;http&lt;/code&gt; command (in this case the  protocol is missing):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nu&amp;gt; http get --full www.google.com
Error: nu::shell::unsupported_input

  × Unsupported input
   ╭─[entry #64:1:1]
 1 │ http get --full www.google.com
   · ────┬───        ───────┬──────
   ·     │                  ╰── value: '"www.google.com"'
   ·     ╰── Incomplete or incorrect URL. Expected a full URL, e.g., https://www.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code above will crash. As mentioned before, we could use &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt;.  But the problem remains, how do we enable the calling code to react to errors?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use std log 

try {
    return http get --full www.google.com # Missing protocol
} catch {
    log error "GET \"www.google.com\" failed."
     # Now what?
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using a result type (and some convenience conversion functions &lt;code&gt;into ok&lt;/code&gt; and &lt;code&gt;into error&lt;/code&gt;), we can write a safe http get function 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;def safe_get [url: string] { 
  try {
    let response = http get --full $url
    $response | into ok
  } catch {
    {url: $url} | into error
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We could use it in our code like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nu&amp;gt; match (safe_get "https://www.google.com") {                                    
    {ok: $response} =&amp;gt; { print $"request succeeded: ($response.status)" },
    {error: $_} =&amp;gt; { print "request failed" }
}
request succeeded: 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for the failure case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;match (safe_get "www.google.com") {                                              
    {ok: $response} =&amp;gt; { print $"request succeeded: ($response.status)" },
    {error: $_} =&amp;gt; { print "request failed" }
}
request failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the calling code can react to failure by disambiguating the return values and processing the attached data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Addendum
&lt;/h2&gt;

&lt;p&gt;Here are the helper functions &lt;code&gt;into ok&lt;/code&gt; and  &lt;code&gt;into error&lt;/code&gt; for completeness sake.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export def "into ok" [value?: any] {
    let v = if $value == null { $in } else { $value }

    {ok: $v}
}

export def "into error" [cause?: any] {
    let c = if $cause == null { $in } else { $cause }

    {error: $c}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  More on Nushell
&lt;/h2&gt;

&lt;p&gt;Check out my blog for more posts about Nushell:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-06-24/" rel="noopener noreferrer"&gt;NUON - Nushell Object Notation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-06-18/" rel="noopener noreferrer"&gt;Wrapper Functions in Nushell&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-05-23/" rel="noopener noreferrer"&gt;Nushell Closures&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-04-08/" rel="noopener noreferrer"&gt;Exporting Local Environments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-04-01/" rel="noopener noreferrer"&gt;Qualified Module Imports&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Dev Containers on Kubernetes With DevSpace</title>
      <dc:creator>Friedrich Kurz</dc:creator>
      <pubDate>Tue, 14 May 2024 05:33:33 +0000</pubDate>
      <link>https://dev.to/fkurz/dev-containers-on-kubernetes-with-devspace-3mf5</link>
      <guid>https://dev.to/fkurz/dev-containers-on-kubernetes-with-devspace-3mf5</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;It is probably undisputed that a robust, easy to use, and quickly set up development environment is one of the key drivers of high developer productivity. Typical productivity boosters include, for example,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduced onboarding time;&lt;/li&gt;
&lt;li&gt;lowered maintenance efforts;&lt;/li&gt;
&lt;li&gt;homogenization of development environments (e.g. across CPU architectures); and&lt;/li&gt;
&lt;li&gt;easy access to required configurations and resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A major concern when talking about development environments is, of course, &lt;strong&gt;packaging&lt;/strong&gt; and &lt;strong&gt;distribution&lt;/strong&gt; because we want to quickly and reliably ramp up—and also tear down—development environments in order to assure the productivity gains described earlier. &lt;/p&gt;

&lt;p&gt;A good way of handling the packaging problem is &lt;strong&gt;containerization&lt;/strong&gt;. This is the case because container images not only may be used to package the tooling required for the development process and allow us to run workloads within a pre-configured environment; but, they may also be versioned, uploaded, and downloaded using established infrastructure components (i.e. image repositories).&lt;/p&gt;

&lt;p&gt;Unsurprisingly, there are quite a lot of tools that operate in the space of providing development setup using containers. Some of the more prominent ones include&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://containers.dev" rel="noopener noreferrer"&gt;Dev Container&lt;/a&gt;,&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devfile.io/" rel="noopener noreferrer"&gt;Devfile&lt;/a&gt;, and&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.gitpod.io/" rel="noopener noreferrer"&gt;Gitpod&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(I personally refer to these tools as &lt;strong&gt;dev container tools&lt;/strong&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  DevSpace
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.devspace.sh/" rel="noopener noreferrer"&gt;DevSpace&lt;/a&gt;, the topic of this post, also falls in the category of dev container tools. It has the advantage over the aforementioned choices, however, that it provides a very generic and customizable approach to bootstrapping development environments. Three of the most striking features to me are the&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;configurable out of the box &lt;strong&gt;SSH server injection&lt;/strong&gt;, as well as the&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;two-way sync&lt;/strong&gt; capability between local host file system and development container file system, and that&lt;/li&gt;
&lt;li&gt;DevSpace development containers &lt;strong&gt;run on Kubernetes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first point is great because remote development using SSH is a tried and true approach with lots of tooling support (e.g. VS Code, IntelliJ, and Neovim all support remote development via SSH). Consequently, it lets developers stay flexible w.r.t. their editor/IDE choice.&lt;/p&gt;

&lt;p&gt;Having a fast and reliable two-way sync mechanism is also great to have because it gives us quick and easy, albeit limited, persistence (limited to the synced directories of course) without having to configure persistent volumes or having to mount directories as you would using only Docker to run a development container. Since containers should be ephemeral, this is a very easy way to keep changes that you want to persist stored away safely without much additional setup.&lt;/p&gt;

&lt;p&gt;As for the last point, running on Kubernetes is a great way to organize and quickly ramp up, as well as tear down, development resources. E.g. we may use Kubernetes namespaces to scope resources for a specific developer; additionally, we may use Kubernetes abstractions to provide and manage access to&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;configuration and credentials, &lt;/li&gt;
&lt;li&gt;downstream network resources (e.g. giving access to third-party systems via external name services), or &lt;/li&gt;
&lt;li&gt;physical resources (like GPUs). &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lastly, you gain the capability to conveniently lift and shift your development from a local Kubernetes cluster to a remote Kubernetes cluster by simply changing the Kubernetes configuration. &lt;/p&gt;

&lt;p&gt;DevSpace, moreover, is an &lt;a href="https://www.cncf.io/projects/devspace/" rel="noopener noreferrer"&gt;official &lt;strong&gt;Cloud Native Computing Foundation&lt;/strong&gt; (CNCF) project&lt;/a&gt; with over 4k stars on GitHub (at the time of writing) and it is, therefore, very likely to be actively worked on in the foreseeable future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Development Workflow with DevSpace
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 This section is a short tutorial illustrating the development workflow with DevSpace. If you want to try it out, please have a look at the proof of concept repository &lt;a href="https://github.com/fkurz/devspace-ssh" rel="noopener noreferrer"&gt;available on my GitHub&lt;/a&gt;. It includes set up instructions for starting a dev container on AWS including code for infrastructure setup and an example dev container Dockerfile.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To use DevSpace, we first have to install it. For example, on an Linux/ARM64 machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; devspace &lt;span class="s2"&gt;"https://github.com/loft-sh/devspace/releases/latest/download/devspace-linux-arm64"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 devspace /usr/local/bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ See &lt;a href="https://www.devspace.sh/docs/getting-started/installation" rel="noopener noreferrer"&gt;here&lt;/a&gt; for more installation options.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Assuming that we already have access to a Kubernetes cluster and that we have pointed &lt;code&gt;kubectl&lt;/code&gt; to use the corresponding context, we should—as a best practice—create a unique namespace—e.g. &lt;code&gt;devspace&lt;/code&gt;—for our development environment and then tell DevSpace to use the targeted context and namespace.&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;$ &lt;/span&gt;kubectl create namespace devspace
namespace/devspace created
&lt;span class="nv"&gt;$ &lt;/span&gt;devspace use namespace devspace
info The default namespace of your current kube-context &lt;span class="s1"&gt;'kind-kind'&lt;/span&gt; has been updated to &lt;span class="s1"&gt;'devspace'&lt;/span&gt;
         To revert this operation, run: devspace use namespace

&lt;span class="k"&gt;done &lt;/span&gt;Successfully &lt;span class="nb"&gt;set &lt;/span&gt;default namespace to &lt;span class="s1"&gt;'devspace'&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;devspace use context &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl config current-context&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done &lt;/span&gt;Successfully &lt;span class="nb"&gt;set &lt;/span&gt;kube-context to &lt;span class="s1"&gt;'arn:aws:eks:eu-central-1:174394581677:cluster/devspace-eks-QbUEJaxD'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 Creating a unique, separate namespace for every developer or feature is a very good way to prevent conflicts. E.g. if we need to change external configuration (e.g. &lt;code&gt;ConfigMaps&lt;/code&gt; or  &lt;code&gt;Secrets&lt;/code&gt;) or change the API of a service during feature development, keeping these changes isolated in a dedicated namespace, prevents breaking the workflow of other developers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We also need to tell DevSpace what kind of dev container to deploy for us.  The way to do this is to use the DevSpace configuration file &lt;code&gt;devspace.yaml&lt;/code&gt;.  Below is an excerpt from the PoC repository mentioned earlier. With a few omissions for the sake of brevity. (In particular, the &lt;code&gt;.pipelines&lt;/code&gt; section that I unfortunately at this point in time only have superficial knowledge on.)&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="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v2beta1&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;devspace&lt;/span&gt;

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

&lt;span class="na"&gt;deployments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;the-dev-container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;helm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;chart&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;component-chart&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://charts.devspace.sh&lt;/span&gt;
      &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&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;${THE_DEV_CONTAINER_IMAGE}"&lt;/span&gt;
            &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IfNotPresent&lt;/span&gt;
            &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;500Mi"&lt;/span&gt;
                &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;500m"&lt;/span&gt;
              &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1Gi"&lt;/span&gt;
                &lt;span class="na"&gt;cpu&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;dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;the-dev-container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;imageSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${THE_DEV_CONTAINER_IMAGE}"&lt;/span&gt;
    &lt;span class="na"&gt;ssh&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;localPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60550&lt;/span&gt; 
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sleep"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;infinity"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;sync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./:/home/dev&lt;/span&gt;

&lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;THE_DEV_CONTAINER_IMAGE&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;env&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu:22.04&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go over the configuration file step by step.&lt;/p&gt;

&lt;p&gt;First, the &lt;code&gt;.deployments&lt;/code&gt; section essentially defines what resources to deploy to the configured Kubernetes cluster. For our use case, the most pivotal deployment is our dev container. The Pod running our dev container will be deployed using Helm (since we specify an &lt;code&gt;.deployments.the-dev-container.helm&lt;/code&gt; object). More specifically, we use the &lt;code&gt;component-chart&lt;/code&gt; Helm chart provided by the DevSpace team. We pass values to the Helm chart with the &lt;code&gt;.deployments.the-dev-container.helm.values&lt;/code&gt; property. Crucially, the dev container image which is parameterized using the DevSpace variable &lt;code&gt;THE_DEV_CONTAINER_IMAGE&lt;/code&gt; specified in the &lt;code&gt;.vars&lt;/code&gt; section, so we are able to reuse the DevSpace configuration for different images.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Note that &lt;code&gt;the-dev-container&lt;/code&gt; was arbitrarily chosen. You can pick whatever name you like best as long as it conforms to syntax requirements. &lt;/p&gt;

&lt;p&gt;ℹ️ The &lt;code&gt;.deployments&lt;/code&gt; section, moreover, allows multiple deployment specifications. We could theoretically also deploy ancillary resources like databases using separate Helm charts.&lt;/p&gt;

&lt;p&gt;ℹ️ Details on the configurable Helm values for the &lt;code&gt;component-chart&lt;/code&gt; Helm chart can be found in the &lt;a href="https://www.devspace.sh/component-chart/docs/configuration/reference" rel="noopener noreferrer"&gt;chart documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Moving on to the &lt;code&gt;.dev&lt;/code&gt; section. We configure a development configuration for the deployment defined earlier, by adding an &lt;code&gt;.dev.the-dev-container&lt;/code&gt; object. We again have to specify the image here (using the &lt;code&gt;THE_DEV_CONTAINER_IMAGE&lt;/code&gt; variable). This time, however, it is used as a selector so that DevSpace can find the Kubernetes Pod running the dev container.&lt;br&gt;
More interestingly, we tell DevSpace to allow SSH access to the deployed dev container by adding an &lt;code&gt;.dev.the-dev-container.ssh&lt;/code&gt; object. It is worthy of note, that the port may be fixed to a specific local port number (via &lt;code&gt;.dev.the-dev-container.ssh.localPort&lt;/code&gt;) so that we don't have to change the configuration of tools trying to connect to our dev container via SSH whenever we redeploy our dev container. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Running &lt;code&gt;devspace dev&lt;/code&gt; with enabled SSH will add an entry in &lt;code&gt;~/.ssh/config&lt;/code&gt; similar to the following one:&lt;/p&gt;


&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# DevSpace Start the-dev-container.devspace.devspace
&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt;-&lt;span class="n"&gt;dev&lt;/span&gt;-&lt;span class="n"&gt;container&lt;/span&gt;.&lt;span class="n"&gt;devspace&lt;/span&gt;.&lt;span class="n"&gt;devspace&lt;/span&gt;
&lt;span class="n"&gt;HostName&lt;/span&gt; &lt;span class="n"&gt;localhost&lt;/span&gt;
&lt;span class="n"&gt;LogLevel&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;
&lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="m"&gt;60550&lt;/span&gt;
&lt;span class="n"&gt;IdentityFile&lt;/span&gt; &lt;span class="s2"&gt;"/home/lima.linux/.devspace/ssh/id_devspace_ecdsa"&lt;/span&gt;
&lt;span class="n"&gt;StrictHostKeyChecking&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt;
&lt;span class="n"&gt;UserKnownHostsFile&lt;/span&gt; /&lt;span class="n"&gt;dev&lt;/span&gt;/&lt;span class="n"&gt;null&lt;/span&gt;
&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;devspace&lt;/span&gt;
&lt;span class="c"&gt;# DevSpace End the-dev-container.devspace.devspace
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;DevSpace also creates a public and private key pair for authentication. This allows us to connect via SSH via&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.devspace/ssh/id_devspace_ecdsa &lt;span class="nt"&gt;-l&lt;/span&gt; devspace &lt;span class="nt"&gt;-p&lt;/span&gt; 11817 localhost
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;or—using the SSH configuration—by simply running &lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh the-dev-container.devspace.devspace
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;p&gt;Additionally, we add a blocking command to the dev container in &lt;code&gt;.dev.the-dev-container.command&lt;/code&gt;, so that our pod doesn't terminate right away.&lt;br&gt;
Lastly, the &lt;code&gt;.dev.the-dev-container.sync&lt;/code&gt; property tells DevSpace to sync file changes from and to our current working directory (&lt;code&gt;./&lt;/code&gt;) to the &lt;code&gt;dev&lt;/code&gt; user's home directory on the dev container (&lt;code&gt;/home/dev&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;As mentioned earlier, we parameterized the dev container image using DevSpace  &lt;a href="https://www.devspace.sh/docs/configuration/variables" rel="noopener noreferrer"&gt;variables&lt;/a&gt; (declared in the &lt;code&gt;.vars&lt;/code&gt; section). So, the last thing we have to do before we can actually launch our dev container with DevSpace, ist to build and upload a suitable dev image to a registry that our cluster has access to. (Or, use a public dev container image.)&lt;/p&gt;

&lt;p&gt;Let's assume for now that we have built and pushed a dev container image tagged &lt;code&gt;174394581677.dkr.ecr.eu-central-1.amazonaws.com/devspace-devcontainer:latest&lt;/code&gt; and that it is available to the provisioned cluster. We then may utilize the variable &lt;code&gt;THE_DEV_CONTAINER_IMAGE&lt;/code&gt; declared in the &lt;code&gt;devspace.yaml&lt;/code&gt; above to deploy a dev container with our custom dev container image using the DevSpace CLI's &lt;code&gt;--var&lt;/code&gt; option:&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;$ &lt;/span&gt;devspace dev &lt;span class="nt"&gt;--var&lt;/span&gt; &lt;span class="nv"&gt;THE_DEV_CONTAINER_IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"174394581677.dkr.ecr.eu-central-1.amazonaws.com/devspace-devcontainer:latest"&lt;/span&gt;
info Using namespace &lt;span class="s1"&gt;'devspace'&lt;/span&gt;
info Using kube context &lt;span class="s1"&gt;'arn:aws:eks:eu-central-1:174394581677:cluster/devspace-eks-3Hij2z5x'&lt;/span&gt;
deploy:the-dev-container Deploying chart /home/lima.linux/.devspace/component-chart/component-chart-0.9.1.tgz &lt;span class="o"&gt;(&lt;/span&gt;the-dev-container&lt;span class="o"&gt;)&lt;/span&gt; with helm...
deploy:the-dev-container Deployed helm chart &lt;span class="o"&gt;(&lt;/span&gt;Release revision: 1&lt;span class="o"&gt;)&lt;/span&gt;
deploy:the-dev-container Successfully deployed the-dev-container with helm
dev:the-dev-container Waiting &lt;span class="k"&gt;for &lt;/span&gt;pod to become ready...
dev:the-dev-container Selected pod the-dev-container-devspace-847f75dd44-9httz
dev:the-dev-container &lt;span class="nb"&gt;sync  &lt;/span&gt;Sync started on: ./ &amp;lt;-&amp;gt; /home/dev
dev:the-dev-container &lt;span class="nb"&gt;sync  &lt;/span&gt;Waiting &lt;span class="k"&gt;for &lt;/span&gt;initial &lt;span class="nb"&gt;sync &lt;/span&gt;to &lt;span class="nb"&gt;complete
&lt;/span&gt;dev:the-dev-container &lt;span class="nb"&gt;sync  &lt;/span&gt;Initial &lt;span class="nb"&gt;sync &lt;/span&gt;completed
dev:the-dev-container ssh   Port forwarding started on: 60550 -&amp;gt; 8022
dev:the-dev-container ssh   Use &lt;span class="s1"&gt;'ssh the-dev-container.devspace.devspace'&lt;/span&gt; to connect via SSH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 Note the lines &lt;code&gt;Sync started on: ./ &amp;lt;-&amp;gt; /home/dev&lt;/code&gt; and &lt;code&gt;Port forwarding started on: 60550 -&amp;gt; 8022&lt;/code&gt; that tell us that the continuous two-way sync of the local working directory to the dev container directory &lt;code&gt;/home/dev&lt;/code&gt; was established; respectively, that the SSH server is listening on the local port 60550 .&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that's it. 🚀 As a simple test, we may run the &lt;code&gt;hostname&lt;/code&gt; command on the remote dev container.&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;$ &lt;/span&gt;ssh the-dev-container.devspace.devspace &lt;span class="s1"&gt;'hostname'&lt;/span&gt;
the-dev-container-devspace-847f75dd44-9httz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As expected from a container running in a Kubernetes Pod, this will return the Pod identifier.&lt;/p&gt;

&lt;p&gt;Assuming sufficient CPU and memory are allocated to the container, we could now continue by attaching shells, and connecting IDE or editor via SSH and start developing. After we're done, we simply detach from the container and run &lt;code&gt;devspace purge&lt;/code&gt; to clear up the deployed resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;DevSpace is a CNC-sponsored project that allows us to easily deploy dev containers on a Kubernetes cluster. When compared to other tools for dev container workflows, it has the advantage that it supports &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;connection via SSH, that it has a&lt;/li&gt;
&lt;li&gt;robust two-way sync mechanism, and that it&lt;/li&gt;
&lt;li&gt;deploys to Kubernetes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As shown above, once a Kubernetes cluster and a container image registry that is accessible from the cluster are available, the development workflow is simple and very flexible w.r.t. to the development tooling. Moreover, we may use Kubernetes abstractions to support some of the desired features of a development environment (e.g. resource isolation via namespaces).&lt;/p&gt;

&lt;p&gt;DevSpace therefore is a very reasonable choice for development teams that have access to both these infrastructure resources since it allows them to leverage the benefits of dev containers with minimal setup requirements and is very flexible w.r.t. to tooling choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  More on DevSpace
&lt;/h2&gt;

&lt;p&gt;Check out my blog for more posts on DevSpace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-05-15/" rel="noopener noreferrer"&gt;Selecting Pods via labels&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>containers</category>
      <category>development</category>
      <category>cncf</category>
    </item>
    <item>
      <title>Detecting Kubernetes API Deprecations with pluto</title>
      <dc:creator>Friedrich Kurz</dc:creator>
      <pubDate>Wed, 27 Jul 2022 13:22:24 +0000</pubDate>
      <link>https://dev.to/fkurz/detecting-kubernetes-api-deprecations-with-pluto-3g2m</link>
      <guid>https://dev.to/fkurz/detecting-kubernetes-api-deprecations-with-pluto-3g2m</guid>
      <description>&lt;p&gt;The Kubernetes API is changing all the time. With these changes come deprecations and eventual removals of parts of the API. To be able to keep an up-to-date Kubernetes cluster version, we have to identify deprecated APIs and update them. This may become tedious in larger clusters with hundreds of resources but tools like pluto can help.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does API deprecation mean in Kubernetes?
&lt;/h2&gt;

&lt;p&gt;The operations governing the lifecycle of all Kubernetes resources are provided via RESTful API endpoints by the &lt;a href="https://kubernetes.io/docs/reference/glossary/?all=true#term-kube-apiserver" rel="noopener noreferrer"&gt;Kubernetes API server&lt;/a&gt;. In other words, the Kubernetes API is the frontend of the &lt;a href="https://kubernetes.io/docs/reference/glossary/?all=true#term-control-plane" rel="noopener noreferrer"&gt;Kubernetes control plane&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%2F2uxxonyleah514n9934q.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%2F2uxxonyleah514n9934q.png" alt="Kubernetes API components" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Resource APIs are associated with URIs like&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/apis/GROUP/VERSION/*&lt;/code&gt; for &lt;strong&gt;cluster-scoped&lt;/strong&gt; resources, and &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/apis/GROUP/VERSION/namespaces/NAMESPACE/*&lt;/code&gt; for &lt;strong&gt;namespace-scoped&lt;/strong&gt; resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some resources are also grouped into the &lt;strong&gt;core group&lt;/strong&gt; (or, &lt;strong&gt;legacy group&lt;/strong&gt;). Those are available via the special API endpoint &lt;code&gt;/api/{version}&lt;/code&gt; (see &lt;a href="https://kubernetes.io/docs/reference/using-api/#api-groups" rel="noopener noreferrer"&gt;&lt;em&gt;API groups&lt;/em&gt;&lt;/a&gt;). Pods, for example, are part of the core group. A request to list all pods in a given namespace &lt;code&gt;my-namespace&lt;/code&gt; is mapped to the following HTTP GET request.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;GET /api/v1/namespaces/my-namespace/pods&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;(See the &lt;a href="https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/" rel="noopener noreferrer"&gt;API documentation for the Pod resource&lt;/a&gt; for reference.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with Kubernetes API deprecations
&lt;/h2&gt;

&lt;p&gt;Kubernetes specifies a &lt;a href="https://kubernetes.io/docs/reference/using-api/deprecation-policy/" rel="noopener noreferrer"&gt;deprecation policy&lt;/a&gt; that defines what it means if parts of an API become &lt;strong&gt;deprecated&lt;/strong&gt;. Essentially, deprecation means that the associated endpoints of the Kubernetes API server are flagged for removal and subsequently deleted. Since the API server governs the resource lifecycle, using a resource with a removed API version, will prevent the deployment of that resource. Consequently, if we fail to update our resource API versions, we will either be stuck with an outdated Kubernetes version; or, updating to the new Kubernetes version will prevent deployments of certain resources. Both are undesirable states since we will either&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;continue using an unstable Kubernetes version, or&lt;/li&gt;
&lt;li&gt;our Kubernetes deployment will be incomplete.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Deploying resources with removed API versions
&lt;/h3&gt;

&lt;p&gt;To get a clearer picture, let's have a look at the second problem and see how Kubernetes responds if we try to deploy a resource using a removed API version. To do this, we spin up a local Kubernetes cluster with &lt;a href="https://k3d.io/" rel="noopener noreferrer"&gt;&lt;code&gt;k3d&lt;/code&gt;&lt;/a&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="nv"&gt;$ &lt;/span&gt;k3d cluster create
INFO[0000] Prep: Network
INFO[0004] Created network &lt;span class="s1"&gt;'k3d-k3s-default'&lt;/span&gt;
INFO[0004] Created image volume k3d-k3s-default-images
INFO[0004] Starting new tools node...
INFO[0005] Pulling image &lt;span class="s1"&gt;'ghcr.io/k3d-io/k3d-tools:5.4.3'&lt;/span&gt;
INFO[0007] Starting Node &lt;span class="s1"&gt;'k3d-k3s-default-tools'&lt;/span&gt;
INFO[0007] Creating node &lt;span class="s1"&gt;'k3d-k3s-default-server-0'&lt;/span&gt;
INFO[0009] Pulling image &lt;span class="s1"&gt;'docker.io/rancher/k3s:v1.23.6-k3s1'&lt;/span&gt;
INFO[0027] Creating LoadBalancer &lt;span class="s1"&gt;'k3d-k3s-default-serverlb'&lt;/span&gt;
INFO[0028] Pulling image &lt;span class="s1"&gt;'ghcr.io/k3d-io/k3d-proxy:5.4.3'&lt;/span&gt;
INFO[0030] Using the k3d-tools node to gather environment information
INFO[0030] Starting new tools node...
INFO[0030] Starting Node &lt;span class="s1"&gt;'k3d-k3s-default-tools'&lt;/span&gt;
INFO[0033] Starting cluster &lt;span class="s1"&gt;'k3s-default'&lt;/span&gt;
INFO[0033] Starting servers...
INFO[0033] Starting Node &lt;span class="s1"&gt;'k3d-k3s-default-server-0'&lt;/span&gt;
INFO[0040] All agents already running.
INFO[0040] Starting helpers...
INFO[0040] Starting Node &lt;span class="s1"&gt;'k3d-k3s-default-serverlb'&lt;/span&gt;
INFO[0049] Injecting records &lt;span class="k"&gt;for &lt;/span&gt;hostAliases &lt;span class="o"&gt;(&lt;/span&gt;incl. host.k3d.internal&lt;span class="o"&gt;)&lt;/span&gt; and &lt;span class="k"&gt;for &lt;/span&gt;3 network members into CoreDNS configmap...
INFO[0051] Cluster &lt;span class="s1"&gt;'k3s-default'&lt;/span&gt; created successfully!
INFO[0051] You can now use it like this:
kubectl cluster-info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then switch Kubernetes context e.g. using &lt;a href="https://github.com/ahmetb/kubectx" rel="noopener noreferrer"&gt;&lt;code&gt;kubectx&lt;/code&gt;&lt;/a&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="nv"&gt;$ &lt;/span&gt;kubectx k3d-k3s-default
Switched to context &lt;span class="s2"&gt;"k3d-k3s-default"&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we take a look at which version the Kubernetes API server is running by executing &lt;code&gt;kubectl version&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="nv"&gt;$ &lt;/span&gt;kubectl version
WARNING: This version information is deprecated and will be replaced with the output from kubectl version &lt;span class="nt"&gt;--short&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;  Use &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yaml|json to get the full version.
Client Version: version.Info&lt;span class="o"&gt;{&lt;/span&gt;Major:&lt;span class="s2"&gt;"1"&lt;/span&gt;, Minor:&lt;span class="s2"&gt;"24"&lt;/span&gt;, GitVersion:&lt;span class="s2"&gt;"v1.24.2"&lt;/span&gt;, GitCommit:&lt;span class="s2"&gt;"f66044f4361b9f1f96f0053dd46cb7dce5e990a8"&lt;/span&gt;, GitTreeState:&lt;span class="s2"&gt;"clean"&lt;/span&gt;, BuildDate:&lt;span class="s2"&gt;"2022-06-15T14:22:29Z"&lt;/span&gt;, GoVersion:&lt;span class="s2"&gt;"go1.18.3"&lt;/span&gt;, Compiler:&lt;span class="s2"&gt;"gc"&lt;/span&gt;, Platform:&lt;span class="s2"&gt;"darwin/amd64"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
Kustomize Version: v4.5.4
Server Version: version.Info&lt;span class="o"&gt;{&lt;/span&gt;Major:&lt;span class="s2"&gt;"1"&lt;/span&gt;, Minor:&lt;span class="s2"&gt;"23"&lt;/span&gt;, GitVersion:&lt;span class="s2"&gt;"v1.23.6+k3s1"&lt;/span&gt;, GitCommit:&lt;span class="s2"&gt;"418c3fa858b69b12b9cefbcff0526f666a6236b9"&lt;/span&gt;, GitTreeState:&lt;span class="s2"&gt;"clean"&lt;/span&gt;, BuildDate:&lt;span class="s2"&gt;"2022-04-28T22:16:18Z"&lt;/span&gt;, GoVersion:&lt;span class="s2"&gt;"go1.17.5"&lt;/span&gt;, Compiler:&lt;span class="s2"&gt;"gc"&lt;/span&gt;, Platform:&lt;span class="s2"&gt;"linux/amd64"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we can see from the &lt;em&gt;Server Version:&lt;/em&gt; output, our k3d cluster is running Kubernetes v1.23. &lt;/p&gt;

&lt;p&gt;By taking a look at the &lt;a href="https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-22" rel="noopener noreferrer"&gt;API deprecation guide&lt;/a&gt; we can see that two versions of the &lt;code&gt;Ingress&lt;/code&gt; API, &lt;strong&gt;extensions/v1beta1&lt;/strong&gt; and &lt;strong&gt;networking.k8s.io/v1beta1&lt;/strong&gt;, were removed in v1.22 of Kubernetes. So let's try to deploy an &lt;code&gt;Ingress&lt;/code&gt; resource with that API version and see what happens. Below we have an example manifest that I shamelessly stole from the &lt;a href="https://kubernetes.io/docs/concepts/services-networking/ingress/#the-ingress-resource" rel="noopener noreferrer"&gt;&lt;em&gt;The Ingress Resource&lt;/em&gt;&lt;/a&gt; section of the official Kubernetes documentation and put it in a file &lt;code&gt;ingress-pre-1.22.yaml&lt;/code&gt;.&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;# ingress-pre-1.22.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;minimal-ingress&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/rewrite-target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-example&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;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/testpath&lt;/span&gt;
        &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
        &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;service&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;test&lt;/span&gt;
            &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then let's try to deploy it to our v1.23 Kubernetes cluster.&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;$ &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; ingress-pre-1.22.yaml
error: resource mapping not found &lt;span class="k"&gt;for &lt;/span&gt;name: &lt;span class="s2"&gt;"minimal-ingress"&lt;/span&gt; namespace: &lt;span class="s2"&gt;""&lt;/span&gt; from &lt;span class="s2"&gt;"ingress-pre-1.22.yaml"&lt;/span&gt;: no matches &lt;span class="k"&gt;for &lt;/span&gt;kind &lt;span class="s2"&gt;"Ingress"&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;version &lt;span class="s2"&gt;"networking.k8s.io/v1beta1"&lt;/span&gt;
ensure CRDs are installed first
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we can see, the API returns an error indicating that &lt;code&gt;networking.k8s.io/v1beta1&lt;/code&gt; does not include an &lt;code&gt;Ingress&lt;/code&gt; type anymore. If we change the API version to &lt;em&gt;networking.k8s.io/v1&lt;/em&gt;, however,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# ingress-1.22.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: minimal-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx-example
  rules:
  - http:
      paths:
      - path: /testpath
        pathType: Prefix
        backend:
          service:
            name: test
            port:
              number: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;our &lt;code&gt;Ingress&lt;/code&gt; will be created without a hitch.&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;$ &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; ingress-1.22.yaml
ingress.networking.k8s.io/minimal-ingress created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Detecting API deprecations with pluto
&lt;/h2&gt;

&lt;p&gt;In a more realistic scenario, we already have resources deployed to our cluster and want to keep their API versions up to date so that we can update our cluster version safely. &lt;/p&gt;

&lt;p&gt;The question therefore is, how do we spot resources with deprecated and soon-to-be-removed API versions? One answer to that question is to check the deprecation guide mentioned earlier and check which resource API versions will be removed in the upcoming Kubernetes update. It is important to note, however, that if we skip several versions, we will have to repeat this check for all versions in between our current and target Kubernetes versions as well.&lt;/p&gt;

&lt;p&gt;In large clusters with dozens of resource types and versions, this can become tedious and error-prone. Luckily, there are tools like &lt;a href="https://github.com/FairwindsOps/pluto" rel="noopener noreferrer"&gt;pluto&lt;/a&gt; by &lt;em&gt;FairwindOps&lt;/em&gt; which assist us in spotting deprecated and soon-to-be-removed resource API versions.&lt;/p&gt;

&lt;p&gt;Let's for example deploy our &lt;code&gt;Ingress&lt;/code&gt; resource to a Kubernetes cluster with an API server version earlier than v1.22 (e.g. v1.19). Creating a k3d cluster with a non-current Kubernetes API version is possible by passing the &lt;code&gt;--image&lt;/code&gt; option to k3d specifying a k3s image for our desired Kubernetes version (e.g. &lt;a href="https://hub.docker.com/layers/k3s/rancher/k3s/v1.19.16-k3s1/images/sha256-14a70ad7fe4402c6639a679522417f3477f6b968f56a71bcec6a07510c0c02a3?context=explore" rel="noopener noreferrer"&gt;v1.19.16-k3s1&lt;/a&gt;, full list of images is available on &lt;a href="https://hub.docker.com/r/rancher/k3s/tags" rel="noopener noreferrer"&gt;Dockerhub&lt;/a&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="nv"&gt;$ &lt;/span&gt;k3d cluster delete
INFO[0000] Deleting cluster &lt;span class="s1"&gt;'k3s-default'&lt;/span&gt;
INFO[0002] Deleting cluster network &lt;span class="s1"&gt;'k3d-k3s-default'&lt;/span&gt;
INFO[0005] Deleting 2 attached volumes...
WARN[0005] Failed to delete volume &lt;span class="s1"&gt;'k3d-k3s-default-images'&lt;/span&gt; of cluster &lt;span class="s1"&gt;'k3s-default'&lt;/span&gt;: failed to find volume &lt;span class="s1"&gt;'k3d-k3s-default-images'&lt;/span&gt;: Error: No such volume: k3d-k3s-default-images -&amp;gt; Try to delete it manually
INFO[0005] Removing cluster details from default kubeconfig...
INFO[0005] Removing standalone kubeconfig file &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if &lt;/span&gt;there is one&lt;span class="o"&gt;)&lt;/span&gt;...
INFO[0005] Successfully deleted cluster k3s-default!
&lt;span class="nv"&gt;$ &lt;/span&gt;k3d cluster create &lt;span class="nt"&gt;--image&lt;/span&gt; rancher/k3s:v1.19.16-k3s1
INFO[0000] Prep: Network
INFO[0003] Created network &lt;span class="s1"&gt;'k3d-k3s-default'&lt;/span&gt;
INFO[0003] Created image volume k3d-k3s-default-images
INFO[0003] Starting new tools node...
INFO[0003] Starting Node &lt;span class="s1"&gt;'k3d-k3s-default-tools'&lt;/span&gt;
INFO[0006] Creating node &lt;span class="s1"&gt;'k3d-k3s-default-server-0'&lt;/span&gt;
INFO[0009] Pulling image &lt;span class="s1"&gt;'rancher/k3s:v1.19.16-k3s1'&lt;/span&gt;
INFO[0018] Creating LoadBalancer &lt;span class="s1"&gt;'k3d-k3s-default-serverlb'&lt;/span&gt;
INFO[0018] Using the k3d-tools node to gather environment information
INFO[0018] Starting new tools node...
INFO[0018] Starting Node &lt;span class="s1"&gt;'k3d-k3s-default-tools'&lt;/span&gt;
INFO[0021] Starting cluster &lt;span class="s1"&gt;'k3s-default'&lt;/span&gt;
INFO[0021] Starting servers...
INFO[0021] Starting Node &lt;span class="s1"&gt;'k3d-k3s-default-server-0'&lt;/span&gt;
INFO[0028] All agents already running.
INFO[0028] Starting helpers...
INFO[0028] Starting Node &lt;span class="s1"&gt;'k3d-k3s-default-serverlb'&lt;/span&gt;
INFO[0037] Injecting records &lt;span class="k"&gt;for &lt;/span&gt;hostAliases &lt;span class="o"&gt;(&lt;/span&gt;incl. host.k3d.internal&lt;span class="o"&gt;)&lt;/span&gt; and &lt;span class="k"&gt;for &lt;/span&gt;3 network members into CoreDNS configmap...
INFO[0039] Cluster &lt;span class="s1"&gt;'k3s-default'&lt;/span&gt; created successfully!
INFO[0039] You can now use it like this:
kubectl cluster-info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We again confirm that the API server is running Kubernetes v1.19 with &lt;code&gt;kubectl version&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="nv"&gt;$ &lt;/span&gt;kubectl version
WARNING: This version information is deprecated and will be replaced with the output from kubectl version &lt;span class="nt"&gt;--short&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;  Use &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yaml|json to get the full version.
Client Version: version.Info&lt;span class="o"&gt;{&lt;/span&gt;Major:&lt;span class="s2"&gt;"1"&lt;/span&gt;, Minor:&lt;span class="s2"&gt;"24"&lt;/span&gt;, GitVersion:&lt;span class="s2"&gt;"v1.24.2"&lt;/span&gt;, GitCommit:&lt;span class="s2"&gt;"f66044f4361b9f1f96f0053dd46cb7dce5e990a8"&lt;/span&gt;, GitTreeState:&lt;span class="s2"&gt;"clean"&lt;/span&gt;, BuildDate:&lt;span class="s2"&gt;"2022-06-15T14:22:29Z"&lt;/span&gt;, GoVersion:&lt;span class="s2"&gt;"go1.18.3"&lt;/span&gt;, Compiler:&lt;span class="s2"&gt;"gc"&lt;/span&gt;, Platform:&lt;span class="s2"&gt;"darwin/amd64"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
Kustomize Version: v4.5.4
Server Version: version.Info&lt;span class="o"&gt;{&lt;/span&gt;Major:&lt;span class="s2"&gt;"1"&lt;/span&gt;, Minor:&lt;span class="s2"&gt;"19"&lt;/span&gt;, GitVersion:&lt;span class="s2"&gt;"v1.19.16+k3s1"&lt;/span&gt;, GitCommit:&lt;span class="s2"&gt;"da16869555775cf17d4d97ffaf8a13b70bc738c2"&lt;/span&gt;, GitTreeState:&lt;span class="s2"&gt;"clean"&lt;/span&gt;, BuildDate:&lt;span class="s2"&gt;"2021-11-04T00:55:24Z"&lt;/span&gt;, GoVersion:&lt;span class="s2"&gt;"go1.15.14"&lt;/span&gt;, Compiler:&lt;span class="s2"&gt;"gc"&lt;/span&gt;, Platform:&lt;span class="s2"&gt;"linux/amd64"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
WARNING: version difference between client &lt;span class="o"&gt;(&lt;/span&gt;1.24&lt;span class="o"&gt;)&lt;/span&gt; and server &lt;span class="o"&gt;(&lt;/span&gt;1.19&lt;span class="o"&gt;)&lt;/span&gt; exceeds the supported minor version skew of +/-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's apply our &lt;code&gt;Ingress&lt;/code&gt; with the manifest specifying the deprecated API version again.&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;$ &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; ingress-pre-1.22.yaml
Warning: networking.k8s.io/v1beta1 Ingress is deprecated &lt;span class="k"&gt;in &lt;/span&gt;v1.19+, unavailable &lt;span class="k"&gt;in &lt;/span&gt;v1.22+&lt;span class="p"&gt;;&lt;/span&gt; use networking.k8s.io/v1 Ingress
ingress.networking.k8s.io/minimal-ingress created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we can see, we already get a helpful deprecation warning that recommends using &lt;code&gt;networking.k8s.io/v1&lt;/code&gt; instead of &lt;code&gt;networking.k8s.io/v1beta1&lt;/code&gt;. For now, we assume, however, that our resources already have been deployed to a running cluster and we want to detect those resources (without applying them again).&lt;/p&gt;

&lt;p&gt;This is where &lt;a href="https://pluto.docs.fairwinds.com/installation/" rel="noopener noreferrer"&gt;tools like &lt;code&gt;pluto&lt;/code&gt;&lt;/a&gt; become handy to detect the use of deprecated resource API version.&lt;/p&gt;

&lt;p&gt;Pluto is available on multiple platforms and via a variety of package managers. We'll use &lt;a href="https://github.com/devops-works/binenv" rel="noopener noreferrer"&gt;binenv&lt;/a&gt; here.&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;$ &lt;/span&gt;binenv &lt;span class="nb"&gt;install &lt;/span&gt;pluto
2022-07-21T19:47:18+02:00 WRN version &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s2"&gt;"pluto"&lt;/span&gt; not specified&lt;span class="p"&gt;;&lt;/span&gt; using &lt;span class="s2"&gt;"5.8.0"&lt;/span&gt;
fetching pluto version 5.8.0 100% |█████████████████████████████████████████████████████████████████████████████████████████████████████████████████| &lt;span class="o"&gt;(&lt;/span&gt;11/11 MB, 9.578 MB/s&lt;span class="o"&gt;)&lt;/span&gt;
2022-07-21T19:47:22+02:00 INF &lt;span class="s2"&gt;"pluto"&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;5.8.0&lt;span class="o"&gt;)&lt;/span&gt; installed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a couple of ways we can hand over resource manifests to pluto in order to detect deprecated API versions: for example via directory scan or by direct input. Additionally, there is a convenient integration with Helm that checks our releases for deprecations. &lt;/p&gt;

&lt;h3&gt;
  
  
  Directory scan
&lt;/h3&gt;

&lt;p&gt;Using a &lt;strong&gt;directory scan&lt;/strong&gt; requires that we know where to find the Kubernetes manifests for our cluster. In our case, this is pretty simple since we only have two simple manifests and they are both in the current directory. The following command makes pluto run a directory scan and detect our deprecated api version.&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;$ &lt;/span&gt;pluto detect-files &lt;span class="nt"&gt;--directory&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--target-versions&lt;/span&gt; &lt;span class="nv"&gt;k8s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v1.22.0
NAME              KIND      VERSION                     REPLACEMENT            REMOVED   DEPRECATED
minimal-ingress   Ingress   networking.k8s.io/v1beta1   networking.k8s.io/v1   &lt;span class="nb"&gt;true      true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that we can specify our target version(s) using the &lt;code&gt;--target-versions&lt;/code&gt; option. If we were to target Kubernetes v1.15.0 instead, pluto would return the empty list, because our CRD API version was deprecated only later on in v1.16.0.&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;$ &lt;/span&gt;pluto detect-files &lt;span class="nt"&gt;--directory&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--target-versions&lt;/span&gt; &lt;span class="nv"&gt;k8s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v1.15.0
No output to display
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Direct input
&lt;/h3&gt;

&lt;p&gt;Using &lt;strong&gt;direct input&lt;/strong&gt;, we can just pipe a resource manifest directly into pluto. This is particularly useful if we want to scan the resources already deployed to the cluster (and going over the manifests is too complicated). Since our CRD is already deployed we can obtain the resource manifest with &lt;code&gt;kubectl get&lt;/code&gt; and hand it to pluto.&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;$ &lt;/span&gt;kubectl get ingress minimal-ingress &lt;span class="nt"&gt;-o&lt;/span&gt; yaml | pluto detect -
Warning: extensions/v1beta1 Ingress is deprecated &lt;span class="k"&gt;in &lt;/span&gt;v1.14+, unavailable &lt;span class="k"&gt;in &lt;/span&gt;v1.22+&lt;span class="p"&gt;;&lt;/span&gt; use networking.k8s.io/v1 Ingress
NAME              KIND      VERSION              REPLACEMENT            REMOVED   DEPRECATED
minimal-ingress   Ingress   extensions/v1beta1   networking.k8s.io/v1   &lt;span class="nb"&gt;true      true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ You may have noticed that pluto reports the deprecated API version as &lt;code&gt;extensions/v1beta1&lt;/code&gt; instead of &lt;code&gt;networking.k8s.io/v1beta1&lt;/code&gt;. This is the case because the API server in v1.19. apparently normalises &lt;code&gt;networking.k8s.io/v1beta1&lt;/code&gt; to &lt;code&gt;extensions/v1beta1&lt;/code&gt;. The manifest of the deployed &lt;code&gt;Ingress&lt;/code&gt; that is returned by &lt;code&gt;kubectl get ingress minimal-ingress -o yaml&lt;/code&gt; therefore has API version &lt;code&gt;extensions/v1beta1&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Helm releases
&lt;/h3&gt;

&lt;p&gt;If we deploy our resources with &lt;a href="https://helm.sh/" rel="noopener noreferrer"&gt;Helm&lt;/a&gt;, Pluto also provides a &lt;code&gt;detect-helm&lt;/code&gt; subcommand that checks our releases for deprecated API versions. &lt;/p&gt;

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

&lt;p&gt;The Kubernetes API is constantly evolving. To keep our cluster up to date, we constantly have to watch for deprecated and soon-to-be-removed resource API versions. Manual checks are possible thanks to the Kubernetes deprecation guide, but they can become very tedious and error-prone. Tools like pluto allow us to automate API deprecation checks and simplify the effort involved in maintaining up-to-date resource API versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  More on Kubernetes
&lt;/h2&gt;

&lt;p&gt;Check out my blog for more posts on Kubernetes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-05-06-dev-containers-on-kubernetes-with-dev-space/" rel="noopener noreferrer"&gt;DevContainers with DevSpace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-05-06/" rel="noopener noreferrer"&gt;Local Kubernetes with Colima&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-04-22/" rel="noopener noreferrer"&gt;DevContainers with odo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2022-06-18-keeping-environment-specific-helm-configuration-dry/" rel="noopener noreferrer"&gt;Keeping environment-specific Helm configuration DRY&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2021-07-17-some-quick-notes-on-telepresence/" rel="noopener noreferrer"&gt;A couple of notes on Telepresence&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>api</category>
    </item>
    <item>
      <title>GitLab CI/CD Runner Clean-up with Pre-build Scripts</title>
      <dc:creator>Friedrich Kurz</dc:creator>
      <pubDate>Mon, 23 May 2022 19:08:24 +0000</pubDate>
      <link>https://dev.to/fkurz/gitlab-cicd-runner-clean-up-with-pre-build-scripts-4b0g</link>
      <guid>https://dev.to/fkurz/gitlab-cicd-runner-clean-up-with-pre-build-scripts-4b0g</guid>
      <description>&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%2Fvdpm729lljxvfncvuual.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%2Fvdpm729lljxvfncvuual.png" width="800" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;GitLab CI/CD has a powerful but somewhat under-documented pre-build script feature that allows us to execute custom logic before builds are run on a GitLab runner. We’ll have a look at how to utilize the pre-build script to automate Docker system clean-up on GitLab runners.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitLab runners and the problem of automated clean-up
&lt;/h2&gt;

&lt;p&gt;Imagine we work on a project using GitLab CI/CD with a set of demanding criteria such as&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;high degree of automation, &lt;/li&gt;
&lt;li&gt;need for high runner uptime,&lt;/li&gt;
&lt;li&gt;fast build execution, and&lt;/li&gt;
&lt;li&gt;frequent use of Docker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Particularly due to the lack of downtime for clean-up and maintenance, we may eventually run into the well-known problem that GitLab runners will run out of disk space. &lt;/p&gt;

&lt;p&gt;Unfortunately, finding a general solution for GitLab runner clean-up is not particularly easy as indicated by the existence of this, to date, unresolved &lt;a href="https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27332" rel="noopener noreferrer"&gt;issue on the GitLab runner issue tracker&lt;/a&gt;. For instance, if we simply clean up all Docker resources after each build, we won't likely run out of disk space. However, our build times would be much higher because Docker won't be able to &lt;a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache" rel="noopener noreferrer"&gt;leverage its build cache mechanism&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Meanwhile, GitLab's documentation recommends running the &lt;a href="https://gitlab.com/gitlab-org/gitlab-runner/blob/main/packaging/root/usr/share/gitlab-runner/clear-docker-cache" rel="noopener noreferrer"&gt;clear-docker-cache script&lt;/a&gt; once a week via cron as a workaround. Using the cron approach is also fairly simple and will moreover slow down our builds less frequently. On the flip side, however, we will now have to provide our runners with sufficient disk space for a full week (or, whatever interval the cron job is set to run on) which might be excessive and is moreover hard to guess correctly.&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%2Ft1qbnv5c1sx2pp21k75j.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%2Ft1qbnv5c1sx2pp21k75j.png" alt="Image description" width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As noted in the unresolved issue that I mentioned earlier, the GitLab suggested way of managing disk space also has at least two more problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;it only addresses &lt;strong&gt;images&lt;/strong&gt;, and it is&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;indiscriminate&lt;/strong&gt; meaning that it may end up cleaning up frequently used images as well.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is problematic for a couple of reasons. First of all, other cached Docker resources—like volumes and containers—are not targeted by the clean-up script. Moreover, it may result in a build slow-down because frequently used images are being cleaned up and have to be rebuilt. Finally—since the script is run by cron—, there also is the intricate problem of race conditions between the clean-up script and build jobs: since cron runs asynchronous to build job executions, our clean-up job may inadvertently crash pipelines because it cleans up images some jobs depend upon. ^[For example if we build our images and push them to our registry in separate jobs.] This problem is of course mitigated by running the clean-up script only once a week but developers will have to keep in mind that pipelines may fail once in a while due to missing images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Determining Docker disk usage and cleaning up Docker cache
&lt;/h2&gt;

&lt;p&gt;So, what to do? First of all, let's look at what tools are available to determine Docker disk usage as well as trigger clean-up of resources that occupy disk space.&lt;/p&gt;

&lt;p&gt;To get an indication of the current disk usage of Docker, we can run &lt;code&gt;docker system df&lt;/code&gt;. Here's an example output from my local machine:&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;$ &lt;/span&gt;docker system &lt;span class="nb"&gt;df
&lt;/span&gt;TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          2         2         216.6MB   0B &lt;span class="o"&gt;(&lt;/span&gt;0%&lt;span class="o"&gt;)&lt;/span&gt;
Containers      2         2         84.83kB   0B &lt;span class="o"&gt;(&lt;/span&gt;0%&lt;span class="o"&gt;)&lt;/span&gt;
Local Volumes   5         5         506.7MB   0B &lt;span class="o"&gt;(&lt;/span&gt;0%&lt;span class="o"&gt;)&lt;/span&gt;
Build Cache     0         0         0B        0B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we can see, Docker helpfully lists the disk space taken up by each of its resource classes.&lt;/p&gt;

&lt;p&gt;With some light BASH acrobatics, we can work this into a test that tells us if Docker disk space usage is below a given limit (see &lt;a href="https://gist.github.com/fkurz/d84e5117d31c2b37a69a2951561b846e#file-is_docker_disk_space_usage_above_limit-sh" rel="noopener noreferrer"&gt;&lt;code&gt;is_docker_disk_space_usage_above_limit.sh&lt;/code&gt;&lt;/a&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="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# --&lt;/span&gt;
&lt;span class="c"&gt;# Test if Docker daemon disk space usage is above a given limit in bytes.&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Example 1: Docker disk space usage is above limit &lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;#    $ source is_docker_disk_space_usage_above_limit.sh&lt;/span&gt;
&lt;span class="c"&gt;#    $ is_docker_disk_space_usage_above_limit 1&lt;/span&gt;
&lt;span class="c"&gt;#    Docker disk space usage is above limit (actual: 1050000005B, limit: 1B)&lt;/span&gt;
&lt;span class="c"&gt;#    $ printf $?&lt;/span&gt;
&lt;span class="c"&gt;#    0&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Example 2: Docker disk space usage is below or equal to limit&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;#    $ source is_docker_disk_space_usage_above_limit.sh&lt;/span&gt;
&lt;span class="c"&gt;#    $ is_docker_disk_space_usage_above_limit 1000000000000&lt;/span&gt;
&lt;span class="c"&gt;#    Docker disk space usage is below or equal to limit (actual: 1050000005B, limit: 1000000000000B)&lt;/span&gt;
&lt;span class="c"&gt;#    $ printf $?&lt;/span&gt;
&lt;span class="c"&gt;#    1&lt;/span&gt;
&lt;span class="c"&gt;# &lt;/span&gt;
&lt;span class="c"&gt;# --&lt;/span&gt;
iec_string_to_bytes&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;iec_string&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
      &lt;span class="nv"&gt;iec_format_pattern&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'([0-9.]+)\s*([kMGTP]?B)'&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;iec_string&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;iec_format_pattern&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"Input string has invalid format (received: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, expected: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;)."&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;iec_format_pattern&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="k"&gt;fi 

  &lt;/span&gt;&lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;number_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_REMATCH&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;iec_unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_REMATCH&lt;/span&gt;&lt;span class="p"&gt;[2]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;

  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;iec_unit&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in 
    &lt;/span&gt;B&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1&lt;span class="p"&gt;;;&lt;/span&gt;
    kB&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000&lt;span class="p"&gt;;;&lt;/span&gt;
    MB&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000000&lt;span class="p"&gt;;;&lt;/span&gt;
    GB&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000000000&lt;span class="p"&gt;;;&lt;/span&gt;
    TB&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000000000000&lt;span class="p"&gt;;;&lt;/span&gt;
    PB&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000000000000000&lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;

  &lt;span class="c"&gt;# We use scale=0 here to drop the (redundant) decimal points.&lt;/span&gt;
  &lt;span class="c"&gt;# This only works with division so we divide by one.&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"scale=0;%s * %s/1&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;number_value&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factor&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | bc 
&lt;span class="o"&gt;}&lt;/span&gt;

calculate_docker_total_disk_space_usage&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;bc_expression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;
    &lt;span class="nv"&gt;disk_space_used&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker system &lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{{.Size}}'&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;disk_space_used_by_resource &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;disk_space_used&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; 
    &lt;span class="c"&gt;# shellcheck disable=SC2086&lt;/span&gt;
    &lt;span class="nv"&gt;disk_space_used_by_resource_bytes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;iec_string_to_bytes &lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;disk_space_used_by_resource&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;bc_expression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;bc_expression&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; + &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;disk_space_used_by_resource_bytes&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;done 

  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;bc_expression&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | bc
&lt;span class="o"&gt;}&lt;/span&gt;

is_docker_disk_space_usage_above_limit&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;disk_space_limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
    &lt;span class="nv"&gt;docker_disk_space_used&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;calculate_docker_total_disk_space_usage&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;docker_disk_space_usage_is_above_limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s &amp;gt; %s\n'&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;docker_disk_space_used&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;disk_space_limit&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; | bc &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="c"&gt;# Note that bc returns 1 if the comparison is true and 0 otherwise.&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;docker_disk_space_usage_is_above_limit&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 1 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then 
    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"Docker disk space usage is above limit (actual: %sB, limit: %sB)"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;docker_disk_space_used&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;disk_space_limit&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;0
  &lt;span class="k"&gt;fi 

  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"Docker disk space usage is below or equal to limit (actual: %sB, limit: %sB)"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;docker_disk_space_used&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;disk_space_limit&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;1
&lt;span class="o"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Schematically, we can run this script in BASH as follows to trigger our clean-up logic:&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="c"&gt;# Check if docker disk space usage is above a given limit and run clean-up logic if it is&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;is_docker_disk_space_usage_above_limit &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;docker_disk_space_usage_limit&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c"&gt;# run clean up&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Docker uses too much disk space, we may then proceed to remove unused resources via the Docker CLI. The most simple way to do this is by running &lt;a href="https://docs.docker.com/config/pruning/#prune-everything" rel="noopener noreferrer"&gt;&lt;code&gt;docker system prune -af --volumes&lt;/code&gt;&lt;/a&gt;. Pruning the system with these parameters will simply clean up &lt;strong&gt;all&lt;/strong&gt; dangling and unused images, containers, networks, as well as volumes. &lt;/p&gt;

&lt;p&gt;In case we need a more elaborate clean-up logic, Docker CLI also has individual commands to free the cache of unused images, containers, networks, and volumes which support filters. E.g. &lt;code&gt;docker image prune&lt;/code&gt; can be used to only clean up images that are older than 24 hours by running&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker image prune &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"until=24h"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Runner hooks to the rescue
&lt;/h2&gt;

&lt;p&gt;Now that we have a way how to find out if Docker is running out of disk space and how to trigger clean-up, we can think about when to run our logic. Per the requirements of our GitLab CI/CD set-up—as stated earlier—, we want to run&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;custom&lt;/strong&gt; clean up logic,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pre-emptively&lt;/strong&gt; (to avoid runner failures due to lack of disk space), and&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;in sync&lt;/strong&gt; with pipeline execution (to prevent randomly failing pipelines).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Luckily, GitLab CI/CD provides a couple of script hooks that let us execute code in various stages of pipeline execution. Specifically, &lt;em&gt;pre-clone&lt;/em&gt;, &lt;em&gt;post-clone&lt;/em&gt;, &lt;em&gt;pre-build&lt;/em&gt;, and &lt;em&gt;post-build&lt;/em&gt; (see &lt;em&gt;The [[runners]] section&lt;/em&gt; in  &lt;a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section" rel="noopener noreferrer"&gt;&lt;em&gt;Advanced Configuration&lt;/em&gt;&lt;/a&gt;). &lt;/p&gt;

&lt;p&gt;Both the pre-build as well as the post-build hooks make sense in our scenario as both &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;run synchronous with pipeline jobs (either before or after a job), &lt;/li&gt;
&lt;li&gt;pre-emptively allow us to clean up resources (either before the current job or the next job), and &lt;/li&gt;
&lt;li&gt;provide a mechanism to define custom clean-up logic. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We choose the pre-build hook here. &lt;/p&gt;

&lt;p&gt;To register our pre-build script, we have to configure our GitLab runners using their configuration file like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/gitlab-runner/config.toml&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="nn"&gt;[[runners]]&lt;/span&gt;
  &lt;span class="c"&gt;# ...&lt;/span&gt;
  &lt;span class="py"&gt;pre_build_script&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'''
    # execute clean-up script
  '''&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having added the &lt;em&gt;pre_build_script&lt;/em&gt; property, our GitLab runners will now execute our clean-up script before each job. &lt;/p&gt;

&lt;p&gt;This is unfortunately not a perfect, general solution either—as will be discussed later in &lt;em&gt;Prerequisites and limitations of the pre-build script approach&lt;/em&gt;—but let's look at how to implement the pre-build clean-up technique first.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick test drive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installing Docker and gitlab-runner
&lt;/h3&gt;

&lt;p&gt;To test our setup we will install and configure GitLab runner on an AWS EC2 instance. (Obviously, any other similar cloud infrastructure as a service solution would work too.)  GitLab runner binaries are available for multiple platforms. We pick an Ubuntu 20.4 machine here to use the Linux binaries.&lt;/p&gt;

&lt;p&gt;After logging into our EC2 instance&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="p"&gt;-pair-pem-file&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;-user&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;-instance-address&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we first want to install Docker. E.g. &lt;a href="https://docs.docker.com/engine/install/ubuntu/" rel="noopener noreferrer"&gt;using the official convenience install script&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com &lt;span class="nt"&gt;-o&lt;/span&gt; /home/ubuntu/get-docker.sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;sh /home/ubuntu/get-docker.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ Note that using the convenience script is not recommended for production builds. Neither is it generally a good idea to execute a downloaded script file with sudo. But since we trust the source here and only run a test, it's not a big deal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Verify Docker installation success by running e.g. &lt;code&gt;docker --version&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="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
Docker version 20.10.16, build aa7e414
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's execute the script shown below to install &lt;code&gt;gitlab-runner&lt;/code&gt;. ^[The GitLab runner installation script is also available from &lt;em&gt;Settings &amp;gt; CI/CD &amp;gt; Runners &amp;gt; Specific runners&lt;/em&gt; of a GitLab project for reference.]&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
  # Download the binary for your system
  sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

  # Give it permission to execute
  sudo chmod +x /usr/local/bin/gitlab-runner

  # Create a GitLab Runner user
  sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

  # Install and run as a service
  sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
  sudo gitlab-runner start
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then let's register our runner with GitLab by running&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;gitlab-runner register 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;gitlab-runner&lt;/code&gt; tool will guide us through the process and ask for some configuration details. We have to enter the URL of our GitLab instance (e.g. &lt;a href="https://gitlab.com" rel="noopener noreferrer"&gt;https://gitlab.com&lt;/a&gt; for the public GitLab) and a registration token (that can be copied from &lt;em&gt;Settings &amp;gt; CI/CD &amp;gt; Runners &amp;gt; Specific runners&lt;/em&gt;). We moreover pick the Docker executor (&lt;em&gt;docker&lt;/em&gt;) since it is—at least in my experience—the most common one as well as the &lt;em&gt;docker:20.10.16&lt;/em&gt; image to be able to run Docker builds within our pipeline jobs. ^[Full disclosure, I tried the SSH executor for simplicity's sake but was not able to make it work due to connection problems.] Also, when prompted for tags, we enter &lt;em&gt;gl-cl&lt;/em&gt; to be able to run our pipeline jobs on exactly this machine.&lt;/p&gt;

&lt;p&gt;At the end of the registration process, we should see the following confirmation that our runner has been registered.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As described in &lt;a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html" rel="noopener noreferrer"&gt;&lt;em&gt;Advanced configuration&lt;/em&gt;&lt;/a&gt;, the configuration file is stored in &lt;code&gt;/etc/gitlab-runner/config.toml&lt;/code&gt; on Unix systems. We will add the &lt;code&gt;runners.pre_build_script&lt;/code&gt; and &lt;code&gt;runners.docker.volumes&lt;/code&gt; properties shown below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/gitlab-runner/config.toml&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="nn"&gt;[[runners]]&lt;/span&gt;
  &lt;span class="c"&gt;# ...&lt;/span&gt;
  &lt;span class="py"&gt;pre_build_script&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'''
    sh $CLEAN_UP_SCRIPT     
  '''&lt;/span&gt;
  &lt;span class="c"&gt;# ...&lt;/span&gt;
  &lt;span class="nn"&gt;[runners.docker]&lt;/span&gt;
  &lt;span class="c"&gt;# ...&lt;/span&gt;
  &lt;span class="py"&gt;volumes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"/var/run/docker.sock:/var/run/docker.sock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/cache"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;pre_build_script&lt;/code&gt; property uses a little trick and simply executes a reference to a script file path &lt;code&gt;CLEAN_UP_SCRIPT&lt;/code&gt; which we will later add as a  GitLab CI/CD variable of type file. By doing that, we can apply changes and test our clean-up script without having to connect to our runner. The &lt;code&gt;volumes&lt;/code&gt; property mounts the host machine's Docker socket in our container so we clean up resources on the host machine rather than only in the container (&lt;a href="https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-socket-binding" rel="noopener noreferrer"&gt;&lt;em&gt;Docker in Docker&lt;/em&gt; via Docker socket binding&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring our GitLab CI/CD pipeline
&lt;/h3&gt;

&lt;p&gt;The file variable &lt;code&gt;CLEAN_UP_SCRIPT&lt;/code&gt; has to be defined in the &lt;em&gt;Settings &amp;gt; CI/CD &amp;gt; Variables&lt;/em&gt; section of our project for this to work. As shown in the next section. Let's add the following clean-up script for now.&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;set&lt;/span&gt; &lt;span class="nt"&gt;-eo&lt;/span&gt; pipefail
apk update 
apk upgrade 
apk add bash curl
curl https://gist.githubusercontent.com/fkurz/d84e5117d31c2b37a69a2951561b846e/raw/a39d6adb1aaede5df2fc54c1882618bcea9f01e0/is_docker_disk_space_usage_above_limit.sh &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/is_docker_disk_space_above_limit.sh
bash &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; || printf "&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;Clean-up failed."
  source /tmp/is_docker_disk_space_above_limit.sh
  if is_docker_disk_space_usage_above_limit 2000000000; then
    printf "&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;Running clean up...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"
    docker system prune -af --volumes
  else 
    printf "&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;Skipping clean up...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"
  fi 
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure to select &lt;em&gt;File&lt;/em&gt; as &lt;em&gt;Type&lt;/em&gt; when defining the variable.&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%2Fw3lz3jbxymxhe7gb64fg.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%2Fw3lz3jbxymxhe7gb64fg.png" alt="Image description" width="800" height="585"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that we install &lt;code&gt;bash&lt;/code&gt; and &lt;code&gt;curl&lt;/code&gt; in the pre-build script for simplicity's sake. This implies that both tools are installed before every job that is processed on this runner. In a real scenario, we'd naturally want to provide a custom image that has all the required tools installed to speed up the pre-build script's execution.&lt;/p&gt;

&lt;p&gt;Now let's add a sample &lt;code&gt;gitlab-ci.yaml&lt;/code&gt; to our project which creates a large one GB image and will therefore eventually trigger clean-up. (Code is &lt;a href="https://gitlab.com/fkurz/gitlab-runner-cleanup" rel="noopener noreferrer"&gt;available on GitLab&lt;/a&gt;.)&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="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;build&lt;/span&gt;

&lt;span class="na"&gt;build-job&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;build&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gl-cl&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;echo "Generating random nonsense..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./scripts/generate-random-nonsense.sh&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Building random nonsense image..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./scripts/build-random-nonsense-image.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To limit our pipelines to our new runner, we use &lt;a href="https://docs.gitlab.com/ee/ci/runners/configure_runners.html#use-tags-to-control-which-jobs-a-runner-can-run" rel="noopener noreferrer"&gt;tag selectors&lt;/a&gt; and pick our previously registered runner via the &lt;code&gt;gl-cl&lt;/code&gt; tag. Now we may finally run our pipeline a couple of times to see the effect of our pre-build script. Depending on the runner's disk size, we should see a couple of jobs without clean-up followed eventually by a run that contains a log similar to this one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Docker disk space usage is above limit (actual: 2361821460B, limit: 2000000000B)&lt;br&gt;
Running clean up...&lt;br&gt;
Deleted Containers:&lt;br&gt;
9a126aead174a15a4f76f2cb5744e36aff30741cc6ab0ac5044837aaee946496&lt;br&gt;
2fa51dc3e7f5b1e3bec63a635c266552f9b02eb74015a9683f7cbf13418a12eb&lt;/p&gt;

&lt;p&gt;Deleted Images:&lt;br&gt;
untagged: registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper:x86_64-febb2a09&lt;br&gt;
untagged: registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper@sha256:edc1bf6ab9e1c7048d054b270f79919eabcbb9cf052b3e5d6f29c886c842bfed&lt;br&gt;
deleted: sha256:c20c992e5d83348903a6f8d18b4005ed1db893c4f97a61e1cd7a8a06c2989c40&lt;br&gt;
deleted: sha256:873201b44549097dfa61fa4ee55e5efe6e8a41bbc3db9c6c6a9bfad4cb18b4ea&lt;br&gt;
untagged: random-nonsense-image-1653227274:latest&lt;br&gt;
deleted: sha256:67fde47d8b24ee105be2ea3d5f04d6cd0982d9db2f1c934b3f5b3675eb7a626f&lt;br&gt;
deleted: sha256:1a310f85590c46c1e885278d1cab269f07033fefdab8f581f06046787cd6156e&lt;br&gt;
untagged: alpine:latest&lt;br&gt;
untagged: alpine@sha256:4edbd2beb5f78b1014028f4fbb99f3237d9561100b6881aabbf5acce2c4f9454&lt;br&gt;
untagged: random-nonsense-image-1653226909:latest&lt;br&gt;
deleted: sha256:b5923f3fb6dd2446d18d75d5fbdb4d35e5fca888bd88aef8174821c0edfcb87f&lt;br&gt;
deleted: sha256:59150b0202d2d5f75ec54634b4d8b208572cbeec9c5519a9566d2e2e6f2c13f3&lt;br&gt;
deleted: sha256:0ac33e5f5afa79e084075e8698a22d574816eea8d7b7d480586835657c3e1c8b&lt;/p&gt;

&lt;p&gt;Total reclaimed space: 2.059GB&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This output indicates that our pre-build script was executed when Docker's disk space usage reached a value above 2GB and clean-up was triggered successfully (freeing in this case roughly 2GB of disk space).&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites and limitations of the pre-build script approach
&lt;/h2&gt;

&lt;p&gt;It's probably easy to see that our pre-build script approach fulfills the requirements we laid out for it. I.e. it &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;runs synchronously before pipeline jobs, it&lt;/li&gt;
&lt;li&gt;pre-emptively cleans-up unused resources, and it&lt;/li&gt;
&lt;li&gt;allows us to provide a custom clean-up logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nonetheless, there are still a few limitations left to consider. &lt;/p&gt;

&lt;p&gt;First of all, &lt;code&gt;bash&lt;/code&gt; must be available during pre-build script execution if we want to use the &lt;a href="https://gist.github.com/fkurz/d84e5117d31c2b37a69a2951561b846e#file-is_docker_disk_space_usage_above_limit-sh" rel="noopener noreferrer"&gt;&lt;code&gt;is_docker_disk_space_usage_above_limit.sh&lt;/code&gt;&lt;/a&gt; script because it uses some BASHisms. Moreover, since we use the Docker executor, we need to use some kind of runner image that has the Docker CLI installed (such as the official Docker base image we used in our test earlier). Writing a custom image to use as the base image to run our pipelines takes care of and reduces the severity of this problem, but it's still something that has to be addressed. &lt;/p&gt;

&lt;p&gt;Another thing to keep in mind is that using Docker disk space usage is only an approximation (i.e. lower than) the system disk space usage. Consequently, we have to find a good value for the limit that triggers our clean-up logic to not run out of disk space on the machine anyway.&lt;/p&gt;

&lt;p&gt;Also, it may still be tricky to pick the right clean-up logic. For instance, if we just run the &lt;code&gt;docker system prune -af --volumes&lt;/code&gt; as in our test, we may delete images that are required by subsequent jobs in more complex pipelines. Excluding certain images from clean-up—for instance, those built in the last 24 hours—may be able to alleviate this exact problem. However, in more complicated pipelines, we will likely need a more elaborate clean-up logic.&lt;/p&gt;

&lt;p&gt;Lastly, there are still edge cases even with the pre-build clean-up script approach where our runners will run out of disk space. Off the top of my head, if the limit is too high, a runner might still end up running out of disk space because the job run produces more resources than available free space.&lt;/p&gt;

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

&lt;p&gt;As we've seen, we can use GitLab CI/CD's pre-build script hook to clean up GitLab runners in sync with job execution, pre-emptively to avoid breaking pipelines, as well as using custom clean-up logic. That being said, the pre-build script clean-up approach is not perfect, because it cannot avoid all situations where a runner will run out of disk space. Nonetheless, I think it is still a more elegant way to handle clean-up of GitLab runners than maintenance downtimes or the cron job approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  More on GitLab CI/CD
&lt;/h2&gt;

&lt;p&gt;Check out my blog for more posts about GitLab CI/CD:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-08-22/" rel="noopener noreferrer"&gt;Make jobs interruptible to not waste runner compute time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-07-12/" rel="noopener noreferrer"&gt;Prevent running on tag push&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-04-09/" rel="noopener noreferrer"&gt;How to simulate running on the main branch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-04-08/" rel="noopener noreferrer"&gt;Rule refactoring with the &lt;code&gt;!reference&lt;/code&gt; tag&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://friedrichkurz.me/posts/2024-03-30/" rel="noopener noreferrer"&gt;ARM64 container scanning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.friedrichkurz.me/posts/2024-03-28/" rel="noopener noreferrer"&gt;GitLab CI/CD variable string interpolation expressions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gitlab</category>
      <category>ci</category>
      <category>cleanup</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
