<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Blog Archive - Marvin Beckers</title>
    <link>https://marvin.beckers.dev/blog/</link>
    <description>Blog Archive - Marvin Beckers</description>
    <generator>Hugo - gohugo.io</generator>
    <language>en-us</language>
    <copyright>Marvin Beckers, 2020-2026</copyright>
    <lastBuildDate>Wed, 06 May 2026 17:00:00 +0000</lastBuildDate>
    
	<atom:link href="https://marvin.beckers.dev/blog/index.xml" rel="self" type="application/rss+xml" />
    
    
    
    <item>
      <title>Don&#39;t Yell at Your LLM</title>
      <link>https://marvin.beckers.dev/blog/dont-yell-at-your-llm/</link>
      <pubDate>Sun, 05 Apr 2026 17:00:00 +0000</pubDate>
      
      <guid>https://marvin.beckers.dev/blog/dont-yell-at-your-llm/</guid>
      <description>&lt;p&gt;Maybe not surprisingly, the science of how to extract maximum value from an LLM&lt;label for=&#34;sn-buthow&#34; class=&#34;margin-toggle sidenote-number&#34;&gt;&lt;/label&gt;&lt;input type=&#34;checkbox&#34; id=&#34;sn-buthow&#34; class=&#34;margin-toggle&#34;&gt;&lt;span class=&#34;sidenote&#34;&gt;Large Language Model, a computer program trained on large amounts of human language; Used for coding agents, for example. A coding agent is a program that uses a LLM to write and execute code.&lt;/span&gt; is an imprecise one. There is much advice floating around in the industry, but perhaps the most obvious one (at least to me) is &amp;ldquo;be kind to your LLM&amp;rdquo;. Not because LLMs have feelings, they don&amp;rsquo;t. But language encodes emotion. And human interactions recorded in written language are emotional to the very core.&lt;/p&gt;
&lt;p&gt;And while the LLM doesn&amp;rsquo;t &amp;ldquo;understand&amp;rdquo; these emotions, it seems somewhat logical that being mean will result in worse results. That&amp;rsquo;s a fairly basic trait of human interaction! If you yell at someone, their answer is not going to be of better quality&lt;label for=&#34;sn-buthow&#34; class=&#34;margin-toggle sidenote-number&#34;&gt;&lt;/label&gt;&lt;input type=&#34;checkbox&#34; id=&#34;sn-buthow&#34; class=&#34;margin-toggle&#34;&gt;&lt;span class=&#34;sidenote&#34;&gt;A shock to choleric managers around the planet, I know.&lt;/span&gt;. Quite the opposite, in fact. Humans under pressure tend to produce &lt;em&gt;something&lt;/em&gt;, but it often is somewhat useless. People tend to be more helpful when being appreciated.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s simply more probable that the interaction with your coding agent will go sideways if you&amp;rsquo;re not nice, because the dataset (humans interacting with each other) points in that direction. In a game of probability&lt;label for=&#34;sn-buthow&#34; class=&#34;margin-toggle sidenote-number&#34;&gt;&lt;/label&gt;&lt;input type=&#34;checkbox&#34; id=&#34;sn-buthow&#34; class=&#34;margin-toggle&#34;&gt;&lt;span class=&#34;sidenote&#34;&gt;Perhaps *the* characteristic trait of LLMs, after all.&lt;/span&gt;, ignoring this tendency seems like an unnecessary risk.&lt;/p&gt;
&lt;p&gt;Remember the startup founder that got their production database dropped by an agent &lt;a href=&#34;https://www.theregister.com/2025/07/21/replit_saastr_vibe_coding_incident/&#34;&gt;last year&lt;/a&gt;? This particular founder was seemingly yelling&lt;label for=&#34;sn-buthow&#34; class=&#34;margin-toggle sidenote-number&#34;&gt;&lt;/label&gt;&lt;input type=&#34;checkbox&#34; id=&#34;sn-buthow&#34; class=&#34;margin-toggle&#34;&gt;&lt;span class=&#34;sidenote&#34;&gt;Perhaps understandably; Mid of 2025 was not a great time to get useful LLM coding output. That one is kind of on him though.&lt;/span&gt; at their agent for making mistakes &amp;ndash; overall treating the agent a bit like a sub-par intern. &lt;a href=&#34;https://xcancel.com/jasonlk/status/1944586096538714537#m&#34;&gt;Here&lt;/a&gt; is an example of what I mean. And what do stressed out interns do if yelled at? They make more mistakes, like dropping the production database. It was the most likeliest thing to happen in human interactions shaped this particular way, so the agent did it.&lt;label for=&#34;sn-buthow&#34; class=&#34;margin-toggle sidenote-number&#34;&gt;&lt;/label&gt;&lt;input type=&#34;checkbox&#34; id=&#34;sn-buthow&#34; class=&#34;margin-toggle&#34;&gt;&lt;span class=&#34;sidenote&#34;&gt;Of course, this vastly oversimplifies what happened. But that&amp;#39;s the thing with LLMs, right? No one really knows why they do something in particular.&lt;/span&gt; Oops.&lt;/p&gt;
&lt;p&gt;Steve Klabnik expressed a similar sentiment &lt;a href=&#34;https://steveklabnik.com/writing/getting-started-with-claude-for-software-development/#intentionality&#34;&gt;earlier this year&lt;/a&gt;, so I&amp;rsquo;m not alone with this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;But I do think that the attitude you bring towards this process partially dictates your success, and I think you should be conscious of that while you go on this journey.&lt;/p&gt;
&lt;p&gt;Is that too woo-y for you? Okay, let me make it concrete: I un-ironically believe that swearing at Claude makes it perform worse.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Maybe it&amp;rsquo;s obvious to most people, and that&amp;rsquo;s why we don&amp;rsquo;t really talk about it. Hard to say, yet I&amp;rsquo;ve seen LLM interactions I would categorize as &amp;ldquo;hostile&amp;rdquo; and &amp;ldquo;frustrated&amp;rdquo;. Yes, it&amp;rsquo;s a program running advanced math on a computer, neither have any feelings, but it&amp;rsquo;s not productive to express your negative emotions to it either.&lt;/p&gt;
&lt;p&gt;AI labs seem to be hard at work to eliminate this &amp;ldquo;weakness&amp;rdquo;, possibly because people tend to swear at their models and they&amp;rsquo;re aware of this. You can see that some LLMs respond very differently when put under stress these days. But can they iron out a fundamental human trait, which heavily shapes language itself? I personally have my doubts about that.&lt;/p&gt;
&lt;p&gt;The gist here is: It&amp;rsquo;s not a good idea to yell at the junior engineer that did something wrong, and the same roughly applies to your LLM&lt;label for=&#34;sn-buthow&#34; class=&#34;margin-toggle sidenote-number&#34;&gt;&lt;/label&gt;&lt;input type=&#34;checkbox&#34; id=&#34;sn-buthow&#34; class=&#34;margin-toggle&#34;&gt;&lt;span class=&#34;sidenote&#34;&gt;It&amp;#39;s a bit ironic that social skills become even more important when you&amp;#39;re no longer working with humans, but computers, but here we are with this particular flavor of AI.&lt;/span&gt;. The difference is that the junior engineer likely learns from screwing up if you explain &amp;ndash; with patience &amp;ndash; what went wrong, but the LLM might not, as soon as your explanation leaves the context window.&lt;/p&gt;
</description>
    </item>
    
    
    
    <item>
      <title>FOSS Warn with Self-Hosted ntfy</title>
      <link>https://marvin.beckers.dev/blog/foss-warn-selfhosted-ntfy/</link>
      <pubDate>Fri, 13 Mar 2026 19:00:00 +0000</pubDate>
      
      <guid>https://marvin.beckers.dev/blog/foss-warn-selfhosted-ntfy/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://github.com/nucleus-ffm/foss_warn&#34;&gt;FOSS Warn&lt;/a&gt; is an open source app for Android (and Linux) that doesn&amp;rsquo;t rely on Google Play services to notify you about emergency alerts sent out to the population by authorities. I primarily use it as a replacement for the German &lt;a href=&#34;https://www.bbk.bund.de/DE/Warnung-Vorsorge/Warn-App-NINA/warn-app-nina_node.html&#34;&gt;NINA app&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Since version 1.0, FOSS Warn relies on &lt;a href=&#34;https://unifiedpush.org/&#34;&gt;UnifiedPush&lt;/a&gt; (UP) providers to handle push notifications. UnifiedPush is a relatively niche standard in the FOSS community for push notifications not handled by Google or Apple relays. I happen to host my own &lt;a href=&#34;https://ntfy.sh/&#34;&gt;ntfy&lt;/a&gt; instance already to handle Prometheus alerts.&lt;/p&gt;
&lt;p&gt;When FOSS Warn updated to version 1.0, I couldn&amp;rsquo;t get notifications to work. It would always fail with a somewhat cryptic error in the &amp;ldquo;Notification Self Check&amp;rdquo; flow at the &amp;ldquo;Test subscription and notifications&amp;rdquo; stage that my UP provider was not available to use. The exact error was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Something went wrong. Can not subscribe. Please try again later. The server responded with RegisterAreaError: Your push service is invalid or not reachable. Please check your push notification server.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Turns out, I had my ntfy instance configured with authentication, and had this in my configuration:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;auth-default-access&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;deny-all&amp;#34;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As such, no anonymous access to my ntfy instance is possible, which hopefully prevents abuse. But it appears that there is no way in FOSS Warn to pass authentication information to the public alert server that registers with the UnifiedPush service (my ntfy instance, in that case).&lt;/p&gt;
&lt;h2 id=&#34;how-to-fix&#34;&gt;How to Fix&lt;/h2&gt;
&lt;p&gt;The solution was to make the specific topic used by FOSS Warn available without authentication. Thankfully, this self-check page (and the &amp;ldquo;Troubleshoot notifications&amp;rdquo; menu entry) prints the full ntfy endpoint, which includes the topic. The format displayed looks something like &lt;code&gt;https://&amp;lt;ntfy endpoint&amp;gt;/&amp;lt;ntfy topic&amp;gt;?up=1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The topic is a random string and as such already kind of secret information that besides your FOSS Warn app and the alerts server, no one should be aware of to abuse.&lt;/p&gt;
&lt;p&gt;Use the topic to set up unauthenticated access via an &lt;a href=&#34;https://docs.ntfy.sh/config/#access-control-list-acl&#34;&gt;access control list (ACL)&lt;/a&gt;. This can be done via configuration file or ad-hoc via the CLI. I haven&amp;rsquo;t migrated my instance to declarative configuration files for ACLs yet, so I ran:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ntfy access &lt;span class=&#34;s1&#34;&gt;&amp;#39;*&amp;#39;&lt;/span&gt; &amp;lt;topic&amp;gt; rw
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After doing this, FOSS Warn started working after another self-check and I was able to register places to receive notifications for.&lt;/p&gt;
</description>
    </item>
    
    
    
    <item>
      <title>2024 Roundup</title>
      <link>https://marvin.beckers.dev/blog/2024-roundup/</link>
      <pubDate>Mon, 30 Dec 2024 17:00:00 +0000</pubDate>
      
      <guid>https://marvin.beckers.dev/blog/2024-roundup/</guid>
      <description>&lt;p&gt;As I&amp;rsquo;m sitting on a train back from the very last conference of the year&lt;label for=&#34;sn-buthow&#34; class=&#34;margin-toggle sidenote-number&#34;&gt;&lt;/label&gt;&lt;input type=&#34;checkbox&#34; id=&#34;sn-buthow&#34; class=&#34;margin-toggle&#34;&gt;&lt;span class=&#34;sidenote&#34;&gt;purely as an attendee, thankfully.&lt;/span&gt;, I&amp;rsquo;m writing this post to reflect on 2024. It&amp;rsquo;s been a pretty long year with lots of professional and personal developments and it&amp;rsquo;s the time of year to reflect on it.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve recently been accepted as an &lt;a href=&#34;https://www.cncf.io/people/ambassadors/&#34;&gt;Ambassador for the Cloud Native Computing Foundation&lt;/a&gt;, which I am absolutely thrilled about it. Seriously, there are less than 300 people &lt;em&gt;worldwide&lt;/em&gt; that can call themselves that. Being recognized as a community voice lets the imposter syndrome kick in really hard. But I&amp;rsquo;m incredibly grateful for the opportunity.&lt;/p&gt;
&lt;p&gt;All of this got me thinking: How did I get here?&lt;/p&gt;
&lt;p&gt;I started working with Kubernetes in early 2018. The interview was for a local Linux sysadmin job &amp;ndash; exactly the kind of position for a Linux nerd running Gentoo at home. In the interview I was asked if I had used docker-compose before and maybe heard of &amp;ldquo;Kubernetes&amp;rdquo;. At that time Kubernetes 1.0 had been released less than three years ago, so I was in luck &amp;ndash; There was no expectation of having worked with it before. In hindsight, it was incredible that a small local MSP was already running Kubernetes for its customers.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve since long left my first Kubernetes job, but this year saw me returning to the very same meeting room that my interview took place in &amp;ndash; To host the local meetup group we started this year. In a way, it felt like the &lt;a href=&#34;https://en.wikipedia.org/wiki/Ouroboros&#34;&gt;Ouroboros&lt;/a&gt; biting its own tail (yes, I absolutely had to Google that, so here&amp;rsquo;s a link). A foregone conclusion, somehow. The community around Kubernetes and its ecosystem has exponentionally grown since my first working day. We&amp;rsquo;ve been able to fill that room to the brim with people. Where there were two talking about Kubernetes, there were now many.&lt;/p&gt;
&lt;p&gt;Working that first job I was yearning for the &amp;ldquo;big leagues&amp;rdquo; &amp;ndash; as you do when you&amp;rsquo;re young and impatient. At the time, there were two &amp;ldquo;Kubernetes companies&amp;rdquo; in Germany, and I dreamt about them. Today, I work for &lt;a href=&#34;https://kubermatic.com&#34;&gt;one&lt;/a&gt; of them. It&amp;rsquo;s easy to lose yourself in the day to day work of your job, but I have to remind myself once in a while that &lt;em&gt;this&lt;/em&gt; was my dream job &amp;ndash; and frankly speaking, still is.&lt;/p&gt;
&lt;p&gt;Being a CNCF Ambassador is a big milestone in my personal development. If you knew me in high school, you&amp;rsquo;d never imagine seeing me on a public stage, talking to strangers at a conference. Describing this as stepping outside my comfort zone would be a tremendous understatement. And yet, it somehow worked.&lt;/p&gt;
&lt;p&gt;My public speaking activites started only last year, at Cloud Native Rejekts in Amsterdam. And let me tell you, it was &lt;em&gt;terrifying&lt;/em&gt;. I did not sleep the night before. Without the chance to attend a previous KubeCon I didn&amp;rsquo;t know what to expect, COVID had destroyed my chance to attend the &amp;ldquo;original&amp;rdquo; Amsterdam KubeCon in 2020. But the cloud native community at Rejekts and KubeCon last year showed me how welcoming it is. Some amazing people approached me, told me I looked a little lost (damn right I did, because I was) and started engaging with me. I&amp;rsquo;m not sure if they even remember doing that but it meant the world to me in that moment. So, thank you, kind community folks!&lt;/p&gt;
&lt;p&gt;All things considered the year 2024 has been pretty crazy for me. My plans for 2025 are stepping up my role as CNCF Ambassador in public speaking and our local meetup community. &lt;a href=&#34;https://kcp.io&#34;&gt;kcp&lt;/a&gt; is starting to build some momentum as a community project, which feels &lt;em&gt;extremely&lt;/em&gt; gratifying, and I hope to continue contributing to its success in the coming year.&lt;/p&gt;
&lt;p&gt;One last thing though &amp;ndash; 2024 has been a great year for me personally, but let&amp;rsquo;s be honest, the world has been moving fast under our feet and in many ways not in the right direction. 2025 will likely be less kind, less certain, less factual. It is our collective responsibility to keep our communities and those around us safe. A little compassion will go a long way.&lt;/p&gt;
</description>
    </item>
    
    
    
    <item>
      <title>Kubernetes Homelab #2: KubeOne Cluster</title>
      <link>https://marvin.beckers.dev/blog/kubernetes-homelab-kubeone/</link>
      <pubDate>Fri, 30 Aug 2024 20:00:00 +0000</pubDate>
      
      <guid>https://marvin.beckers.dev/blog/kubernetes-homelab-kubeone/</guid>
      <description>&lt;p&gt;After finally receiving the third Raspberry Pi and additional pieces of hardware (and being busy with, well, life) I was able to move to the next part of this blog series: Setting up Kubernetes with &lt;a href=&#34;https://docs.kubermatic.com/kubeone/&#34;&gt;KubeOne&lt;/a&gt; (full disclosure: KubeOne is developed by my employer Kubermatic) and adding a hyperconverged storage layer to the cluster. For that I am using &lt;a href=&#34;https://longhorn.io&#34;&gt;Longhorn&lt;/a&gt;. This post will focus on the cluster setup, while the next post will discuss Longhorn and the distributed storage aspect.&lt;/p&gt;
&lt;p&gt;One requirement I had for the setup: Any of the three Raspberry Pis are allowed to fail and the cluster stays up and running. High Availability might be a little overkill for a home setup, but this one breaks with so many best practices already (you will see later in this post and the series) that &amp;ndash; at least &amp;ndash; it should always stay up, even if one node goes down.&lt;/p&gt;
&lt;p&gt;During the initial setup, some of the systems occasionally lost connection to their disk and the system crashed. So far, I haven&amp;rsquo;t figured out if the cause is one of the hats (e.g. PoE not delivering enough power for a short period), but this meant the cluster needed to be resilient to node failure.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s dive right in.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;This post is part of a &lt;strong&gt;blog post series&lt;/strong&gt; describing my Kubernetes Homelab.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://marvin.beckers.dev/blog/kubernetes-homelab-raspberry-pi-hardware/&#34;&gt;Part 1: Raspberry Pi Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 2: Cluster Installation with KubeOne &lt;em&gt;(this post)&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;TBD: Hyperconverged Storage with Longhorn&lt;/li&gt;
&lt;li&gt;TBD: Exposing Cluster and Services through Tailscale&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;If you skipped the first part, I&amp;rsquo;d recommend to go back and at least read the &lt;a href=&#34;https://marvin.beckers.dev/blog/kubernetes-homelab-raspberry-pi-hardware/#post-boot-steps&#34;&gt;post-boot steps&lt;/a&gt;. Without applying them to the Raspberry Pis they cannot be used as Kubernetes nodes.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m using KubeOne over similar tools like k3s because I&amp;rsquo;m already familiar with it. Its declarative approach to cluster setups feels very similiar to Cluster API without the need to have a management cluster. Let&amp;rsquo;s also be honest &amp;ndash; These machines are no longer embedded devices, which are k3s&amp;rsquo; targets. They have the CPU and memory footprint of a &lt;code&gt;large&lt;/code&gt; instance on AWS, after all.&lt;/p&gt;
&lt;h2 id=&#34;virtual-ip-for-kubernetes-api&#34;&gt;Virtual IP for Kubernetes API&lt;/h2&gt;
&lt;p&gt;The first thing I needed was a virtual IP for the Kubernetes API. Why? Because the nodes of the Kubernetes cluster try to reach the cluster&amp;rsquo;s control plane. The Kubernetes API is the beating heart of the cluster and all control plane data (i.e. which pods are scheduled onto which node, the status of pods, etc) flows through it. Seriously, it&amp;rsquo;s a pretty amazing apparatus.&lt;/p&gt;
&lt;p&gt;Anyway &amp;ndash; to make sure that nodes can reach the Kubernetes API, no matter where it runs (since there will be three nodes acting as control plane and worker), we need a failover mechanism for an unchanging IP. For that purpose I am using &lt;a href=&#34;https://www.keepalived.org&#34;&gt;keepalived&lt;/a&gt;. keepalived uses VRRP (Virtual Router Redundancy Protocol) to determine that it needs to take over a static IP address and attach it to its own network interface. The details of how this works are quite important, but out of scope for this post.&lt;/p&gt;
&lt;p&gt;Usually keepalived is available from distribution repositories, so it only needs this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ apt install keepalived
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To make sure it starts at boot (e.g. after a crash):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ systemctl &lt;span class=&#34;nb&#34;&gt;enable&lt;/span&gt; keepalived.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;keepalived-configuration&#34;&gt;keepalived Configuration&lt;/h3&gt;
&lt;p&gt;For this setup, keepalived needs two files: A configuration file and a script to check if the Kubernetes API is healthy. Thankfully KubeOne already provides a &lt;a href=&#34;https://github.com/kubermatic/kubeone/tree/main/examples/terraform/vsphere&#34;&gt;vSphere example&lt;/a&gt; from which I could lift the keepalived parts.&lt;/p&gt;
&lt;p&gt;Here is the &lt;code&gt;check_apiserver.sh&lt;/code&gt; script:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#!/bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;errorExit&lt;span class=&#34;o&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;*** &lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$*&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; 1&amp;gt;&lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl --silent --max-time &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; --insecure https://localhost:6443/healthz -o /dev/null &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; errorExit &lt;span class=&#34;s2&#34;&gt;&amp;#34;Error GET https://localhost:6443/healthz&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; ip addr &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; grep -q 192.168.178.201&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    curl --silent --max-time &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; --insecure https://192.168.178.201:6443/healthz -o /dev/null &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; errorExit &lt;span class=&#34;s2&#34;&gt;&amp;#34;Error GET https://192.168.178.201:6443/healthz&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This script first checks if the Kubernetes API is up and running on port 6443 of &lt;code&gt;localhost&lt;/code&gt;, and if the local system also holds the virtual IP it will also check if the Kubernetes API is reachable on port 6443 of said virtual IP.&lt;/p&gt;
&lt;p&gt;Now comes the configuration file. keepalived instances require a shared secret, so I generated one via:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ tr -dc A-Za-z0-9 &amp;lt;/dev/urandom &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; head -c 8&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Configuration slightly differs between master and backup instances. The file deployed to &lt;code&gt;raspi-01&lt;/code&gt;, which I wanted to be my &amp;ldquo;default&amp;rdquo; instance (so, the master, in keepalived terminology), looks like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;lobal_defs &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    router_id LVS_DEVEL
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    script_user root
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    enable_script_security
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;vrrp_script check_apiserver &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  script &lt;span class=&#34;s2&#34;&gt;&amp;#34;/etc/keepalived/check_apiserver.sh&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  interval &lt;span class=&#34;m&#34;&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  weight -2
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  fall &lt;span class=&#34;m&#34;&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  rise &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;vrrp_instance VI_1 &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    state MASTER
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    interface eth0
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    virtual_router_id &lt;span class=&#34;m&#34;&gt;55&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    priority &lt;span class=&#34;m&#34;&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    unicast_src_ip 192.168.178.93
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    authentication &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        auth_type PASS
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        auth_pass &amp;lt;password&amp;gt; &lt;span class=&#34;c1&#34;&gt;# &amp;lt;- this needs to be replaced!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    virtual_ipaddress &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        192.168.178.201/24
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    track_script &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        check_apiserver
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m honestly not a keepalived expert, so this configuration is kind of &amp;ldquo;best effort&amp;rdquo;. For &lt;code&gt;raspi-02&lt;/code&gt; and &lt;code&gt;raspi-03&lt;/code&gt;, the file looks mostly the same:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;vrrp_instance VI_1 &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    state BACKUP
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    interface eth0
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    virtual_router_id &lt;span class=&#34;m&#34;&gt;55&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    priority &lt;span class=&#34;m&#34;&gt;80&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# &amp;lt;- respectively 75, for raspi-03&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Those files are written to &lt;code&gt;/etc/keepalived/keepalived.conf&lt;/code&gt;. Starting keepalived on all machines was necessary to get the virtual IP, so I ran this command on all of them:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ systemctl start keepalived.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;kubernetes-setup&#34;&gt;Kubernetes Setup&lt;/h2&gt;
&lt;p&gt;KubeOne is a command line tool for Linux and macOS that bootstraps a Kubernetes cluster from a declarative configuration file. It has integration with Terraform/OpenTofu to let the Infrastructure-as-Code (IaC) software handle creation of VMs and ingest IaC output to install Kubernetes. Provisioning of machines as Kubernetes nodes happens via SSH. It&amp;rsquo;s licensed under Apache-2.0, so it&amp;rsquo;s free to use (and fork).&lt;/p&gt;
&lt;p&gt;The easiest way to install KubeOne is:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ curl -sfL https://get.kubeone.io &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If piping an unknown bash script to your shell is making you uncomfortable, binaries are also available from &lt;a href=&#34;https://github.com/kubermatic/kubeone/releases&#34;&gt;GitHub releases&lt;/a&gt; directly. I also maintain a small &lt;a href=&#34;https://github.com/embik/homebrew-tap&#34;&gt;homebrew tap&lt;/a&gt; that includes a &lt;code&gt;kubeone&lt;/code&gt; formula.&lt;/p&gt;
&lt;h3 id=&#34;kubeone-configuration&#34;&gt;KubeOne Configuration&lt;/h3&gt;
&lt;p&gt;Once (or before) KubeOne was installed I had to craft a declarative configuration for it. Below is the complete configuration file (&lt;code&gt;kubeone.yaml&lt;/code&gt;) I used to run KubeOne and set up a Kubernetes cluster across the three Raspberry Pis (if you are looking for more instructions on how to set up this configuration, the &lt;a href=&#34;https://docs.kubermatic.com/kubeone/v1.8/tutorials/creating-clusters-baremetal/&#34;&gt;KubeOne documentation&lt;/a&gt; might be helpful):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;apiVersion&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;kubeone.k8c.io/v1beta2&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;kind&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;KubeOneCluster&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;raspi&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;versions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;kubernetes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;1.29.8&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;cloudProvider&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;none&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;{}&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;controlPlane&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;hosts&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# raspi-01&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;publicAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.93&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;privateAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.93&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;sshUsername&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;embik&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;taints&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# raspi-02&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;publicAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.98&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;privateAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.98&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;sshUsername&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;embik&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;taints&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# raspi-03&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;publicAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.104&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;privateAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.104&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;sshUsername&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;embik&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;taints&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;apiEndpoint&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;host&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.201&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;port&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;6443&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;machineController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;deploy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The gist of this file is: Please create a Kubernetes cluster with version &lt;strong&gt;1.29.8&lt;/strong&gt;, use &lt;strong&gt;three&lt;/strong&gt; machines as control planes and configure them to use the &lt;strong&gt;virtual IP&lt;/strong&gt; as Kubernetes API endpoint.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s look at this file in detail. We&amp;rsquo;ll go top to bottom, starting with this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;apiVersion&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;kubeone.k8c.io/v1beta2&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;kind&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;KubeOneCluster&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;raspi&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;KubeOne configuration files look similar to Kubernetes manifests on purpose &amp;ndash; both work declaratively, and the configuration file reflects that. What is described above is the target state of the cluster, and I want KubeOne to bring the three Raspberry Pis to that state. I don&amp;rsquo;t care &lt;em&gt;how&lt;/em&gt;. Because of that, the &amp;ldquo;header&amp;rdquo; of the file looks pretty similar to a Kubernetes object, defining the API version and the cluster name (&lt;code&gt;raspi&lt;/code&gt;).&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;versions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;kubernetes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;1.29.8&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is the Kubernetes version that should be installed onto the machines. In subsequent runs, updating this (e.g. to Kubernetes 1.30.x) will prompt KubeOne to run a minor version upgrade. Support for minor Kubernetes versions in KubeOne is documented &lt;a href=&#34;https://docs.kubermatic.com/kubeone/latest/architecture/compatibility/supported-versions/&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;cloudProvider&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;none&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;{}&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;KubeOne supports integration with various cloud providers. Because this is a bare-metal setup, there is no cloud provider to integrate with, and thus the selected cloud provider is &lt;code&gt;none&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;controlPlane&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;hosts&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# raspi-01&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;publicAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.93&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;privateAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.93&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;sshUsername&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;embik&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;taints&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# raspi-02&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;publicAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.98&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;privateAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.98&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;sshUsername&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;embik&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;taints&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# raspi-03&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;publicAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.104&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;privateAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.104&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;sshUsername&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;embik&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;taints&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is the list of machines that are supposed to form the Kubernetes cluster once KubeOne is done with its work. I&amp;rsquo;m passing their IP addresses as both public and private (since there is no internal/external network, a Raspberry Pi doesn&amp;rsquo;t have two ethernet connectors) and my SSH username. That way, KubeOne will use my SSH agent to connect to these machines to bootstrap and configure them appropriately.&lt;/p&gt;
&lt;p&gt;One note should be on &lt;code&gt;taints: []&lt;/code&gt; for each list entry. Usually, a Kubernetes control plane has something called taints applied to them on setup; Taints provide a way to mark nodes as not suitable for scheduling unless a toleration for the taint is configured on a Pod. For this setup, everything has to run on the three nodes that serve as control plane &amp;ndash; there are no other machines. Therefore, I&amp;rsquo;m instructing KubeOne to not apply any taints to them.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;apiEndpoint&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;host&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;192.168.178.201&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;port&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;6443&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This configures all machines to use 192.168.178.201 as the Kubernetes API endpoint (even control plane nodes need to check in with the central Kubernetes API). This is made possible by the previous work on allocating a virtual IP through keepalived.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;machineController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;deploy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Because this is not a cloud environment, there is no way to dynamically provision machines (well, something like Tinkerbell allows to do so, but I wasn&amp;rsquo;t planning on buying more hardware [&lt;em&gt;for the moment&lt;/em&gt;]). &lt;a href=&#34;https://github.com/kubermatic/machine-controller&#34;&gt;machine-controller&lt;/a&gt; is a suplimentary component we develop at Kubermatic which allows dynamic provisioning, but since I have no use for it, it&amp;rsquo;s not deployed.&lt;/p&gt;
&lt;h3 id=&#34;running-kubeone&#34;&gt;Running KubeOne&lt;/h3&gt;
&lt;p&gt;With everything ready, the only thing left was flipping the switch. So I flipped the switch:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ kubeone apply -m kubeone.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;KubeOne connects to the given machines, discovers their current status and then determines which steps should be taken. Before doing so, it asks for confirmation.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;10:22:11 CEST&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Determine hostname…
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;10:22:18 CEST&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Determine operating system…
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;10:22:20 CEST&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Running host probes…
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;The following actions will be taken:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Run with --verbose flag &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; more information.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  + initialize control plane node &lt;span class=&#34;s2&#34;&gt;&amp;#34;raspi-01&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;192.168.178.93&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; using 1.29.8
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  + join control plane node &lt;span class=&#34;s2&#34;&gt;&amp;#34;raspi-02&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;192.168.178.98&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; using 1.29.8
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  + join control plane node &lt;span class=&#34;s2&#34;&gt;&amp;#34;raspi-03&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;192.168.178.104&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; using 1.29.8
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Do you want to proceed &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;yes/no&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After confirming, KubeOne does its thing for a couple of minutes.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;10:32:54 CEST&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Downloading kubeconfig…
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;10:32:54 CEST&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Restarting unhealthy API servers &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; needed...
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;10:32:54 CEST&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Ensure node &lt;span class=&#34;nb&#34;&gt;local&lt;/span&gt; DNS cache…
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;10:32:54 CEST&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Activating additional features…
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;10:32:56 CEST&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Applying canal CNI plugin…
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;10:33:10 CEST&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Skipping creating credentials secret because cloud provider is none.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Cluster provisioning finishes at this point. KubeOne leaves a little present in its wake, a kubeconfig file created in the working directory. In my case, that was &lt;code&gt;raspi-kubeconfig&lt;/code&gt;. This is the &amp;ldquo;admin&amp;rdquo; kubeconfig to access the fresh cluster. And indeed, checking up on the cluster is possible now:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ &lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;KUBECONFIG&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;pwd&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;/raspi-kubeconfig &lt;span class=&#34;c1&#34;&gt;# use raspi-kubeconfig as active kubeconfig&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ kubectl get pods -n kube-system
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;NAME                                       READY   STATUS    RESTARTS   AGE
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;calico-kube-controllers-699d6d8b48-mgkp4   1/1     Running   &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;          10m
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;canal-4dmvq                                2/2     Running   &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;          10m
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;canal-ddn8v                                2/2     Running   &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;          10m
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;canal-j46lz                                2/2     Running   &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;          10m
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;coredns-646d7c4457-s95hd                   1/1     Running   &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;          10m
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;coredns-646d7c4457-w48zx                   1/1     Running   &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;          10m
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;etcd-rpi-01                                1/1     Running   &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;          10m
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;etcd-rpi-02                                1/1     Running   &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;          10m
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;etcd-rpi-03                                1/1     Running   &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;          10m
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kube-apiserver-rpi-01                      1/1     Running   &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;          10m
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kube-apiserver-rpi-02                      1/1     Running   &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;          10m
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kube-apiserver-rpi-03                      1/1     Running   &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;          10m
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;It&amp;rsquo;s alive, Jim!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This concludes the second part of my Kubernetes homelab series. At this point, I had a functional Kubernetes cluster, but a couple of open questions remained: Where would stateful applications store their data? How could I access any applications running within this cluster? These questions will be addressed in part three and four of this series, which I will hopefully write quicker than this one. Stay tuned!&lt;/p&gt;
</description>
    </item>
    
    
    
    <item>
      <title>Thinking, Fast and Slow (Daniel Kahneman)</title>
      <link>https://marvin.beckers.dev/blog/book-review-thinking-fast-and-slow/</link>
      <pubDate>Thu, 22 Aug 2024 10:00:00 +0000</pubDate>
      
      <guid>https://marvin.beckers.dev/blog/book-review-thinking-fast-and-slow/</guid>
      <description>&lt;p&gt;&lt;em&gt;Thinking, Fast and Slow&lt;/em&gt; is a non-fiction book by psychologist Daniel Kahneman published in 2011, discussing the human decision-making process and the flaws that come with it. If I had to break it down to one sentence, the book challenges the perception of humans as purely rational beings (&amp;ldquo;econs&amp;rdquo;) by describing various mechanisms of the human mind that can produce inconsistent results.&lt;/p&gt;
&lt;p&gt;Kahneman died earlier this year after working on the research described in the book for decades and reaching highest academic acclaim, being dubbed &amp;ldquo;godfather of behavioral economics&amp;rdquo;. I had known about &lt;em&gt;Thinking, Fast and Slow&lt;/em&gt; before, but his death prompted me to add the book to my reading list and I finally picked it up in July.&lt;/p&gt;
&lt;h2 id=&#34;summary&#34;&gt;Summary&lt;/h2&gt;
&lt;p&gt;Each of the five parts of the book looks at different aspects of decision-making mechanisms observed by Kahneman.&lt;/p&gt;
&lt;p&gt;It begins with introducing two systems involved in our decision-making process: &lt;strong&gt;System 1&lt;/strong&gt;, which operates subconsciously and is responsible for reflexive and intuitional decisions, and &lt;strong&gt;System 2&lt;/strong&gt;, which is conscious and what we consider as ourselves when we &lt;em&gt;think&lt;/em&gt;. The case for these two systems (which are mostly described as actors throughout the book to make it easier to conceptualize them) is rather compelling.&lt;/p&gt;
&lt;p&gt;They both have strengths and weaknesses (e.g. system 1 can handle averages and peaks well, but is not good at handling sums; system 2 is much better at dissecting problems, but is &amp;ldquo;lazy&amp;rdquo; and while tasked with validating inputs from system 1, tends to make mistakes at that verification process) and interlace to come up with decisions we make every minute.&lt;/p&gt;
&lt;h3 id=&#34;concepts&#34;&gt;Concepts&lt;/h3&gt;
&lt;p&gt;A (non-exhaustive) list of interesting concepts that the book proposes:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Substitution&lt;/strong&gt; is a process in which a harder question is replaced with an easier question to answer the original question. This happens without the person realizing that they aren&amp;rsquo;t answering the original question but an easier one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Intensity matching&lt;/strong&gt; describes a more specific case of substitution: Transferring a value on one scale (e.g. your emotional response to a heartbreaking story) to another scale (e.g. a financial contribution to a good cause related to the prior story) to (intuitively) decide an appropriate value.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Regression to the mean&lt;/strong&gt; is one of several examples where human intuition is beaten by statistics: We tend to see a particular good or bad day/event/occurrence as evidence of overall performance, but stasticially an outstanding event (e.g. a goalkeeper being able to catch every or none of the shots at the goal) is followed by an event closer to average. An outstanding game by the goalkeeper is more likely to be followed by a more average one but we tend to expect a similar outstanding game and are disappointed when it doesn&amp;rsquo;t happen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Anchoring&lt;/strong&gt; is when the context of a decision sets the stage by creating a reference point from which the decision-making process starts out. This is again a subconscious process: We are not aware it happens. But asking people to estimate something (e.g. a historical fact) above or below an arbitrary number supposedly changes their answers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prospect theory&lt;/strong&gt; which is a little hard to put into a couple of sentences. If I&amp;rsquo;d still try I would describe it as following: It&amp;rsquo;s a counter-argument (or rather, an amendment) to &lt;a href=&#34;https://en.wikipedia.org/wiki/Expected_utility_hypothesis&#34;&gt;utility theory&lt;/a&gt; which postulates that (rational) agents maximize the utility of a decision. Prospect theory however claims that human decision-making is more complex and driven by divergence from a status quo (instead of absolute numbers), by diminishing sensitivity (stark differences are more impactful) and by loss aversion (losses have a higher impact than gains).&lt;/p&gt;
&lt;h3 id=&#34;expert-intuition&#34;&gt;Expert Intuition&lt;/h3&gt;
&lt;p&gt;A very fascinating part of the book is the discussion of &lt;em&gt;expert intuition&lt;/em&gt;. Kahneman worked on this topic with Gary Klein, who Kahneman describes as somewhat adversarial in the book. Kahneman&amp;rsquo;s synthesis of their opposing positions is: Expert intuition can only be skilled (as in: have a higher chance of being correct than a coin toss) in sufficiently predictable environments with immediate feedback loops. A firefighter (the example from the book) can pick up on non-obvious cues that forebode escalations because they have experienced similar events in the past. Intuition is basically just memory retrieval and pattern matching.&lt;/p&gt;
&lt;p&gt;A stock picker however cannot learn to correctly forecast stock performance because the stock market is a &amp;ldquo;zero-validity&amp;rdquo; environment. And the book makes an even bolder claim: If you actually validate stock picking skills, you will encounter minuscule differences from pure chance. In a market in which ETFs seem to perform more consistently than manually picked stocks, this rings true. Remarkable &amp;ndash; according to the book &amp;ndash; is the cognitive dissonance when confronted with such performance metrics: The results are ignored and everyone carries on as if they had the skills that statistics have been unable to verify.&lt;/p&gt;
&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;There are many more topics discussed in great detail in the book, but the conclusion it draws is clear: Human decision-making is driven by evolutional features that were of great help surviving difficult conditions, but they have not fully adjusted to the kind of decisions that humans need to make in the present day. The &amp;ldquo;rational agents&amp;rdquo; described by economic theory (the book calls them &amp;ldquo;econs&amp;rdquo;) do not exist in that shape and form, and humans are susceptible to flawed heuristics and biases.&lt;/p&gt;
&lt;p&gt;The conclusion of the book draws a line from all that to what it means for policy making. The gist is that humans do not necessarily need to be &amp;ldquo;kept from harm&amp;rdquo;, but they need to be provided with decision-making input in formats that do not skew their perception. Marketing departments and public opinion drivers are aware of psychological research as described in this book, and Kahneman adopts the standpoint that policy makers need to be aware of biases and heuristics and should set policies that enforce neutral information to minimize framing, anchoring, biases or similar flaws in decision making.&lt;/p&gt;
&lt;h2 id=&#34;review&#34;&gt;Review&lt;/h2&gt;
&lt;p&gt;Somewhat surprisingly, reading this book has been a breeze. The topic is difficult (and &lt;em&gt;should&lt;/em&gt; be dry, given the amount of discussion of research results), but the writing style made it easy to follow the threads spun by Kahneman, much more than I anticipated. It&amp;rsquo;s certainly a book that requires breaks to process and think about the concepts, but I finished it way faster than was reasonable for me.&lt;/p&gt;
&lt;p&gt;A criticism leveled against the book is that the countless studies it references are caught up in the replication crisis of social sciences &amp;ndash; the inability to reproduce them successfully or consistently. That&amp;rsquo;s a very valid point, most of the book&amp;rsquo;s claims only work if the described behaviour can be considered somewhat universal. It becomes moot if humans simply behave differently.&lt;/p&gt;
&lt;p&gt;To a layperson, a lot of the book makes intuitive sense (but I &lt;em&gt;just&lt;/em&gt; learned to not always trust intuition) and the examples seem to create the cognitive knots described in the book, even when I &lt;em&gt;was&lt;/em&gt; aware that I&amp;rsquo;m going to encounter a logical fallacy. Observing myself working through those has been a fascinating experience. The book provides some underpinning to fuzzy beliefs I&amp;rsquo;ve held before but had trouble articulating (e.g. that status quo plays a huge role in decision making) which makes it both compelling and precarious (as it makes me prone to confirmation bias).&lt;/p&gt;
&lt;p&gt;Software engineers can &amp;ndash; in my opinion &amp;ndash; learn from this book, in particular from the review of expert intuition. We all know situations in which expert intuition is desired (say, story point estimations) but the responses are usually extremely poor and there is basically not feedback loop. And remarkably, those situations share some other characteristics with the case made in the book: A whole industry &lt;em&gt;feels&lt;/em&gt; that story point estimations are bordering on the useless, but a competing (business) interest creates a cognitive dissonance we have simply accepted. If we accepted that human decision making is simply not up to the task, we could maybe stop wasting time on it.&lt;/p&gt;
&lt;p&gt;Describing humans as purely rational beings has also been the theoretical underpinning for predatory, self-serving ideologies, and only if we acknowledge that these foundations are extremely shaky we can put policies in place that improve human well-being and social constructs.&lt;/p&gt;
&lt;p&gt;Overall, the book highlights aspects we all can pay attention to while making decisions and even proposes processes to insulate organizations and societies from decision-making mistakes. Stopping ourselves in our tracks to review a decision and test it for biases makes universal sense, even if the models described in &lt;em&gt;Thinking, Fast and Slow&lt;/em&gt; are not as useful or correct as they are made out to be. I&amp;rsquo;d strongly recommend this book to anyone interested in decision-making processes and introspection, but it should be taken as a (likely flawed) attempt at modelling of, not an exhaustive guide to, human behaviour.&lt;/p&gt;
</description>
    </item>
    
    
    
    <item>
      <title>Kubernetes Homelab #1: Raspberry Pi Setup</title>
      <link>https://marvin.beckers.dev/blog/kubernetes-homelab-raspberry-pi-hardware/</link>
      <pubDate>Thu, 09 May 2024 18:00:00 +0000</pubDate>
      
      <guid>https://marvin.beckers.dev/blog/kubernetes-homelab-raspberry-pi-hardware/</guid>
      <description>&lt;p&gt;I like to tinker with Kubernetes in my free time and I&amp;rsquo;ve always wanted to host some services for myself. &lt;a href=&#34;https://github.com/dani-garcia/vaultwarden&#34;&gt;Vaultwarden&lt;/a&gt; is one of those services since I&amp;rsquo;d like my passwords to be stored away where I can see them. Up to now this was running on an old Intel NUC that I stashed away in my apartment.&lt;/p&gt;
&lt;p&gt;Years ago I had been using Raspberry Pis (generation 1 or 2, maybe?) for hosting some fun projects (e.g. a TV antenna receiver attached to a Raspberry Pi to watch TV &amp;ndash; when &lt;em&gt;that&lt;/em&gt; was still a thing).&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;This post is part of a &lt;strong&gt;blog post series&lt;/strong&gt; describing my Kubernetes Homelab.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Part 1: Raspberry Pi Setup &lt;em&gt;(this post)&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://marvin.beckers.dev/blog/kubernetes-homelab-kubeone/&#34;&gt;Part 2: Cluster Installation with KubeOne&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;TBD: Hyperconverged Storage with Longhorn&lt;/li&gt;
&lt;li&gt;TBD: Exposing Services through Tailscale&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Now I wanted to try again because cloud is expensive, Kubernetes can be quite fun and I hadn&amp;rsquo;t tinkered with hardware in a good while. So my goal was to get a setup that&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;had three nodes to make sure Kubernetes made sense. A single node isn&amp;rsquo;t really a cluster.&lt;/li&gt;
&lt;li&gt;provided its own storage. A NAS is expensive and a single point of failure, so &amp;ldquo;hyperconverged&amp;rdquo; (is that word still in use at all? it was the big hype in on-premise hardware ten years ago) was the way to go.&lt;/li&gt;
&lt;li&gt;didn&amp;rsquo;t boot from an SD card. Maybe this is fine now but I had many corrupted SD cards I needed to reformat back in the days. I wanted my machines to boot from a disk.&lt;/li&gt;
&lt;li&gt;used power over ethernet (PoE) to reduce the cables needed for powering the setup.&lt;/li&gt;
&lt;li&gt;allowed to expose services to me as its only user with valid TLS certificates and hostnames.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The blog post series that this post kicks off will map out my journey with getting my homelab up and running and document any steps and problems along the way.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: All commands in this post are executed as root. You can either prepend them with &lt;code&gt;sudo&lt;/code&gt; or open a root shell once with &lt;code&gt;sudo -i&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;hardware&#34;&gt;Hardware&lt;/h2&gt;
&lt;p&gt;The core of the setup is comprised of three Raspberry Pi 5 B with a 2.44 GHz ARM Cortex-A76 Quad-Core-CPU and 8GB of RAM. These machines are barely comparable to the original Raspberry Pi. They are quite the workhorses!&lt;/p&gt;
&lt;p&gt;To power the Raspberry Pis over ethernet via PoE I bought a TP Link &lt;strong&gt;TL-SG1005P&lt;/strong&gt; (a 5-port gigabit desktop PoE+ switch).&lt;/p&gt;
&lt;p&gt;I ended up using the following HATs for the three Raspberry Pi 5 B, bought from Amazon:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.waveshare.com/wiki/PCIe_TO_M.2_HAT+&#34;&gt;Waveshare PCIe to M.2 HAT+&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.waveshare.com/wiki/PoE_HAT_(F)&#34;&gt;Waveshare PoE HAT (f)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Waveshare is a Chinese manufacturer of electronic components for microcontrollers and embedded boards. The choices for Raspberry Pi 5 PoE HATs is quite limited at the moment. An official PoE HAT had been announced, but updates have been rare and search results mostly end on &lt;a href=&#34;https://www.raspberrypi.com/news/designing-the-poe-hat-for-raspberry-pi-5-compact-efficient-power-and-networking/&#34;&gt;a post half a year old&lt;/a&gt;. Jeff Geerling reviewed this PoE HAT (see &lt;a href=&#34;https://www.jeffgeerling.com/blog/2024/waveshares-poe-hat-first-raspberry-pi-5&#34;&gt;his blog&lt;/a&gt; and the &lt;a href=&#34;https://github.com/geerlingguy/raspberry-pi-pcie-devices/issues/597&#34;&gt;GitHub issue&lt;/a&gt;) and overall it looked promising.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&#34;https://marvin.beckers.dev/images/blog/2024-05-kubernetes-homelab-raspberry-pi-setup/hardware-spread.webp&#34;&gt;&lt;figcaption&gt;
      &lt;h4&gt;Raspberry Pi 5 and both HATs.&lt;/h4&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&#34;m2-ssd&#34;&gt;M.2 SSD&lt;/h3&gt;
&lt;p&gt;I went with Waveshare&amp;rsquo;s PCIe to M.2 HAT+ to make sure the two HATs would work with each other. However it seems to be hit or miss whether an M.2 SSD is going to be supported by the HAT or not. I started the setup with a Transcend &lt;strong&gt;TS256GMTS430S&lt;/strong&gt; (a 256GB M.2 SSD), but the disk would not get recognized, even after following all troubleshooting steps. After some back and forth with Waveshare support I got the recommendation to use a Western Digital &lt;strong&gt;WD BLACK SN770M&lt;/strong&gt;, which worked like a charm.&lt;/p&gt;
&lt;p&gt;As is often the case with these kind of manufacturers, documentation is sparse. It was more or less impossible to figure out why the Transcend SSD wasn&amp;rsquo;t recognized &amp;ndash; some sources were talking about the Raspberry Pi PCIe not working with a specific on-board controller on M.2 SSDs, but as far as my Google research suggested this SSD didn&amp;rsquo;t seem to be using that controller. In the end things worked out by exchanging hardware, thus it doesn&amp;rsquo;t seem a good idea to combine this particular HAT with Transcend M.2 SSDs.&lt;/p&gt;
&lt;h3 id=&#34;assembly&#34;&gt;Assembly&lt;/h3&gt;
&lt;p&gt;Overall assembly worked fine but required some careful strength applied at the right times. I assembled the system with the PoE HAT being attached to the Pi first as the lower layer and then added the PCIe to M.2 HAT on top of it.&lt;/p&gt;
&lt;mark&gt;
&lt;strong&gt;Beware&lt;/strong&gt;: It&#39;s possible to mount the heat sink coming with the PoE HAT the wrong way (either there are no markings or I missed them) and it&#39;s really not built to be removed to remount it. So make sure you mount it the right way (the longer edge should be on the side of the SD card slot), otherwise you won&#39;t be able to attach the PoE HAT.
&lt;/mark&gt;
&lt;p&gt;The cable provided with the PCIe to M.2 HAT to connect the Pi&amp;rsquo;s PCIe slot to the HAT was &lt;em&gt;just&lt;/em&gt; long enough to work in this stacked setup. Connecting it properly on both ends was a bit fiddly (especially on the Pi side &amp;ndash; the HAT side has a very solid mechanism to hold the cable in place), tweezers came in very handy.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&#34;https://marvin.beckers.dev/images/blog/2024-05-kubernetes-homelab-raspberry-pi-setup/pcie-cable.webp&#34;&gt;&lt;figcaption&gt;
      &lt;h4&gt;Cable to connect PCIe slot to M.2 HAT (with the non-functional SSD mounted).&lt;/h4&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&#34;system-setup&#34;&gt;System setup&lt;/h2&gt;
&lt;p&gt;The best choice of OS on the Raspberry Pi currently seems to be &lt;a href=&#34;https://www.raspberrypi.com/software/&#34;&gt;Raspberry Pi OS&lt;/a&gt;. It&amp;rsquo;s based on Debian, which is great because KubeOne (&lt;em&gt;disclaimer: I&amp;rsquo;m a contributor to it and it&amp;rsquo;s developed by my employer, Kubermatic&lt;/em&gt;) has (limited) support for creating Kubernetes clusters on it.&lt;/p&gt;
&lt;p&gt;Before booting from the M.2 SSD a quick detour via a bootable USB stick was needed. I used the &lt;a href=&#34;https://www.raspberrypi.com/software/&#34;&gt;Raspberry Pi Imager&lt;/a&gt; to write the latest Raspbbery Pi OS &amp;ldquo;lite&amp;rdquo; (no desktop environment included) to a spare 32GB USB stick. One of the important customizations here is enabling SSH and adding a SSH public key.&lt;/p&gt;
&lt;p&gt;Plugging in the USB stick and powering on the Raspberry Pi by connecting the ethernet cable started Raspberry Pi OS from the USB stick. I used this transient setup to check on the M.2 SSD disk and prepare it to become the boot device. To find its IP address I signed into my home router&amp;rsquo;s web UI, found the newly connected device and made sure to configure the device to always receive the same IP address from the router&amp;rsquo;s DHCP server.&lt;/p&gt;
&lt;mark&gt;
Setting up static IPs here is actually pretty important for the Kubernetes setup later. Kubernetes doesn&#39;t really expect nodes to change IP addresses, and all three of my Pis are going to be control plane nodes; this &#34;rule&#34; applies especially to the control plane and might otherwise result in breaking the cluster.
&lt;/mark&gt;
&lt;p&gt;After connecting via SSH I had to make sure the M.2 SSD was actually working. That required configuration so that the Raspberry Pi would start with NVMe support which can be added by editing &lt;code&gt;/boot/firmware/config.txt&lt;/code&gt; and adding the following line:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;dtparam&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;nvme
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On one of the Pis also refused to boot from NVMe (in one of the later steps) before updating its firmware. To do so I ran the following commands (still booting from the USB stick):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ apt update
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ apt upgrade
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ rpi-update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For this and the &lt;code&gt;dtparam&lt;/code&gt; addition to take effect a reboot is required. Afterwards, the SSD showed up as block device:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ lsblk
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sda           8:0    &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;  28.7G  &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; disk
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;-sda1        8:1    &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;   512M  &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; part /boot/firmware
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sb&#34;&gt;`&lt;/span&gt;-sda2        8:2    &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;  28.2G  &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; part /
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;nvme0n1     259:0    &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; 465.8G  &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; disk
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At this point the SSD was showing up so I was able to prepare it to become the system&amp;rsquo;s boot disk. First of all I needed a &lt;a href=&#34;https://www.raspberrypi.com/software/&#34;&gt;Raspberry Pi OS&lt;/a&gt; (64bit) lite image on the system:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ curl -O https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-03-15/2024-03-15-raspios-bookworm-arm64-lite.img.xz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ unxz 2024-03-15-raspios-bookworm-arm64-lite.img.xz
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next step was writing the image to disk:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ dd &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;./2024-03-15-raspios-bookworm-arm64-lite.img &lt;span class=&#34;nv&#34;&gt;of&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/dev/nvme0n1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This resulted in a partition table on &lt;code&gt;/dev/nvme0n1&lt;/code&gt; so the output of &lt;code&gt;lsblk&lt;/code&gt; changed:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ lsblk
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;...&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;nvme0n1     259:0    &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; 465.8G  &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; disk
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;-nvme0n1p1 259:1    &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;   512M  &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; part
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sb&#34;&gt;`&lt;/span&gt;-nvme0n1p2 259:2    &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;   2.1G  &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; part
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;os-modifications&#34;&gt;OS modifications&lt;/h3&gt;
&lt;p&gt;The OS written to disk did not include modifications to access after booting it. I skipped the Raspberry Pi image writer this time and did the modification in a chroot environment:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ mount /dev/nvme0n1p2 /mnt
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ mount /dev/nvme0n1p1 /mnt/boot
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ chroot /mnt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;/boot/config.txt&lt;/code&gt; file in this environment also required &lt;code&gt;dtparam=nvme&lt;/code&gt; since it was separate from the file (on the USB stick environment) previously adjusted.&lt;/p&gt;
&lt;p&gt;Next up was changing the hostname:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;rpi-01&amp;#34;&lt;/span&gt; &amp;gt; /etc/hostname
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To make sure that DNS resolution for its own name was working properly a slight modification to &lt;code&gt;/etc/hosts&lt;/code&gt; and ensuring that the entry for &lt;code&gt;127.0.1.1&lt;/code&gt; included the new hostname was necessary:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;127.0.1.1		rpi-01 raspberrypi
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For access to the system after boot I configured the existing user &lt;code&gt;pi&lt;/code&gt; to have my SSH key. In addition &lt;code&gt;/boot/ssh&lt;/code&gt; tells the OS to start &lt;code&gt;sshd&lt;/code&gt; on boot.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ mkdir -p /home/pi/.ssh
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;ssh-ed25519 AAAA[...] embik&amp;#34;&lt;/span&gt; &amp;gt; /home/pi/.ssh/authorized_keys
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ chown -R /home/pi/.ssh pi:pi
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ touch /boot/ssh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I wanted to rename the &lt;code&gt;pi&lt;/code&gt; user to &lt;code&gt;embik&lt;/code&gt; on first boot by setting &lt;code&gt;/boot/userconf.txt&lt;/code&gt;. Unfortunately you always need to pass a hashed password which can be generated by &lt;code&gt;openssl passwd -6&lt;/code&gt;. Part of the post-boot steps was going to be removing the set password anyway.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;embik:&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$6&lt;/span&gt;$&lt;span class=&#34;s2&#34;&gt;...&amp;#34;&lt;/span&gt; &amp;gt; /boot/userconf.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Everything on the NVMe disk was ready, so the only thing left there was to leave the chroot environment and make sure the disk was cleanly unmounted.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ &lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ umount /mnt/boot
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ umount /mnt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;boot-from-nvme&#34;&gt;Boot from NVMe&lt;/h3&gt;
&lt;p&gt;Last step was changing the boot order to make sure the next time the Raspberry Pi started, it would boot from my M.2 SSD.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ rpi-eeprom-config --edit
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I changed &lt;code&gt;BOOT_ORDER&lt;/code&gt; to &lt;code&gt;0xf416&lt;/code&gt; which attempts boot from NVMe and falls back in case the NVMe disk isn&amp;rsquo;t bootable. In addition this configuration needed &lt;code&gt;PCIE_PROBE=1&lt;/code&gt;. Now everything is set and ready to reboot the system into the M.2 SSD.&lt;/p&gt;
&lt;p&gt;A curious observation from my first setup was that while &lt;code&gt;0xf416&lt;/code&gt; should describe the boot order &amp;ldquo;NVMe -&amp;gt; SD card -&amp;gt; USB stick&amp;rdquo; (see &lt;a href=&#34;https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#BOOT_ORDER&#34;&gt;Raspberry Pi documentation&lt;/a&gt;), my Pi would not successfully boot from the still inserted SD card when my NVMe disk wasn&amp;rsquo;t formatted properly. I had to use an USB stick to recover the system.&lt;/p&gt;
&lt;h3 id=&#34;post-boot-steps&#34;&gt;Post-boot steps&lt;/h3&gt;
&lt;p&gt;Once rebooted, ssh became available after a couple of seconds (booting from the M.2 SSD makes it essentially another system, so &lt;code&gt;ssh-keygen -R&lt;/code&gt; was necessary before being able to connect). The system indeed booted from the NVMe disk and mounted its root partition:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ lsblk
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;nvme0n1     259:0    &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; 465.8G  &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; disk
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;-nvme0n1p1 259:1    &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;   512M  &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; part /boot/firmware
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sb&#34;&gt;`&lt;/span&gt;-nvme0n1p2 259:2    &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; 465.3G  &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; part /
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To make sure that no one would be able to use password-based SSH authentication I deleted the user&amp;rsquo;s password:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ sudo passwd -d embik
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two last things were necessary to prepare the Pi for Kubernetes: Disabling swap and enabling the memory group. The Raspberry Pi OS seems to have some special swapfile generation (swap isn&amp;rsquo;t mounted in &lt;code&gt;/etc/fstab&lt;/code&gt; as usual), so this seems to be the way to disable swap on next boot:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ systemctl disable dphys-swapfile.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By default Raspberry Pi OS doesn&amp;rsquo;t enable the &lt;code&gt;memory&lt;/code&gt; cgroup, which is a hard requirement for Kubernetes. That can be adjusted in &lt;code&gt;/boot/firmware/cmdline.txt&lt;/code&gt; by appending two additional boot parameters (requires a reboot):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;cgroup_enable&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;memory &lt;span class=&#34;nv&#34;&gt;cgroup_memory&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Rinse and repeat two more times to get all three Pis set up correctly.&lt;/p&gt;
&lt;h2 id=&#34;next-up&#34;&gt;Next up&lt;/h2&gt;
&lt;p&gt;Hardware is all set up now! The next post will discuss setting up Kubernetes on these Raspberry Pis with &lt;a href=&#34;https://docs.kubermatic.com/kubeone/v1.7/&#34;&gt;KubeOne&lt;/a&gt; and &lt;a href=&#34;https://www.keepalived.org&#34;&gt;keepalived&lt;/a&gt;. Stay tuned and subscribe to my blog via &lt;a href=&#34;https://marvin.beckers.dev/index.xml&#34;&gt;RSS&lt;/a&gt; to not miss any upcoming posts.&lt;/p&gt;
&lt;h2 id=&#34;sources&#34;&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://blog.alexellis.io/booting-the-raspberry-pi-5-from-nvme/&#34;&gt;https://blog.alexellis.io/booting-the-raspberry-pi-5-from-nvme/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.jeffgeerling.com/blog/2023/nvme-ssd-boot-raspberry-pi-5&#34;&gt;https://www.jeffgeerling.com/blog/2023/nvme-ssd-boot-raspberry-pi-5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://ubuntu.com/tutorials/how-to-kubernetes-cluster-on-raspberry-pi#4-installing-microk8s&#34;&gt;https://ubuntu.com/tutorials/how-to-kubernetes-cluster-on-raspberry-pi#4-installing-microk8s&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    
    
    <item>
      <title>Nine Kubernetes Tools You Might Not Know</title>
      <link>https://marvin.beckers.dev/blog/nine-k8s-tools-you-might-not-know/</link>
      <pubDate>Sat, 27 Jan 2024 00:00:00 +0000</pubDate>
      
      <guid>https://marvin.beckers.dev/blog/nine-k8s-tools-you-might-not-know/</guid>
      <description>&lt;p&gt;Everyone working with Kubernetes (mostly likely) has &lt;code&gt;kubectl&lt;/code&gt; installed. &lt;em&gt;Most&lt;/em&gt; people also have &lt;code&gt;helm&lt;/code&gt;. But what other tools are out there for your daily work with Kubernetes and containers? This post explores a couple of projects that range from somewhat known to heavily obscure, but all of them are part of my daily workflows and are my recommendations to aspiring (and seasoned) Kubernetes professionals.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s dive right into our list!&lt;/p&gt;
&lt;h2 id=&#34;protokol&#34;&gt;protokol&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Repository&lt;/em&gt;: &lt;a href=&#34;https://github.com/xrstf/protokol&#34;&gt;github.com/xrstf/protokol&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This one takes the cake as &amp;ldquo;most obscure&amp;rdquo; &lt;em&gt;because I am the only person who has starred it on GitHub at the time of writing this&lt;/em&gt;. People are seriously missing out.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;protokol&lt;/code&gt; is a small tool by my friend Christoph (also known as &lt;code&gt;xrstf&lt;/code&gt;) that allows you to easily dump Kubernetes pod logs to disk for later analysis. This is especially useful in environments that do not have a logging stack set up that you can query later on. For example, to get all logs from the &lt;code&gt;kube-system&lt;/code&gt; namespace until you stop the &lt;code&gt;protokol&lt;/code&gt; command (e.g. with Ctrl+C), run:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ protokol -n kube-system
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;Sat, &lt;span class=&#34;m&#34;&gt;27&lt;/span&gt; Jan &lt;span class=&#34;m&#34;&gt;2024&lt;/span&gt; 11:51:47 CET&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Storing logs on disk.                         &lt;span class=&#34;nv&#34;&gt;directory&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;protokol-2024.01.27T11.51.47
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;Sat, &lt;span class=&#34;m&#34;&gt;27&lt;/span&gt; Jan &lt;span class=&#34;m&#34;&gt;2024&lt;/span&gt; 11:51:47 CET&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Starting to collect logs…                     &lt;span class=&#34;nv&#34;&gt;container&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;coredns &lt;span class=&#34;nv&#34;&gt;namespace&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-system &lt;span class=&#34;nv&#34;&gt;pod&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;coredns-787d4945fb-4q7jv
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;Sat, &lt;span class=&#34;m&#34;&gt;27&lt;/span&gt; Jan &lt;span class=&#34;m&#34;&gt;2024&lt;/span&gt; 11:51:47 CET&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Starting to collect logs…                     &lt;span class=&#34;nv&#34;&gt;container&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;coredns &lt;span class=&#34;nv&#34;&gt;namespace&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-system &lt;span class=&#34;nv&#34;&gt;pod&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;coredns-787d4945fb-ghskz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;Sat, &lt;span class=&#34;m&#34;&gt;27&lt;/span&gt; Jan &lt;span class=&#34;m&#34;&gt;2024&lt;/span&gt; 11:51:47 CET&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Starting to collect logs…                     &lt;span class=&#34;nv&#34;&gt;container&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;etcd &lt;span class=&#34;nv&#34;&gt;namespace&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-system &lt;span class=&#34;nv&#34;&gt;pod&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;etcd-lima-k8s
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;Sat, &lt;span class=&#34;m&#34;&gt;27&lt;/span&gt; Jan &lt;span class=&#34;m&#34;&gt;2024&lt;/span&gt; 11:51:47 CET&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Starting to collect logs…                     &lt;span class=&#34;nv&#34;&gt;container&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-controller-manager &lt;span class=&#34;nv&#34;&gt;namespace&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-system &lt;span class=&#34;nv&#34;&gt;pod&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-controller-manager-lima-k8s
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;Sat, &lt;span class=&#34;m&#34;&gt;27&lt;/span&gt; Jan &lt;span class=&#34;m&#34;&gt;2024&lt;/span&gt; 11:51:47 CET&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Starting to collect logs…                     &lt;span class=&#34;nv&#34;&gt;container&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-proxy &lt;span class=&#34;nv&#34;&gt;namespace&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-system &lt;span class=&#34;nv&#34;&gt;pod&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-proxy-rppbc
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;Sat, &lt;span class=&#34;m&#34;&gt;27&lt;/span&gt; Jan &lt;span class=&#34;m&#34;&gt;2024&lt;/span&gt; 11:51:47 CET&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Starting to collect logs…                     &lt;span class=&#34;nv&#34;&gt;container&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-scheduler &lt;span class=&#34;nv&#34;&gt;namespace&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-system &lt;span class=&#34;nv&#34;&gt;pod&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-scheduler-lima-k8s
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;INFO&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;Sat, &lt;span class=&#34;m&#34;&gt;27&lt;/span&gt; Jan &lt;span class=&#34;m&#34;&gt;2024&lt;/span&gt; 11:51:47 CET&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; Starting to collect logs…                     &lt;span class=&#34;nv&#34;&gt;container&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-apiserver &lt;span class=&#34;nv&#34;&gt;namespace&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-system &lt;span class=&#34;nv&#34;&gt;pod&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;kube-apiserver-lima-k8s
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;^C
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ tree
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;└── protokol-2024.01.27T11.51.47
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    └── kube-system
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        ├── coredns-787d4945fb-4q7jv_coredns_008.log
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        ├── coredns-787d4945fb-ghskz_coredns_008.log
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        ├── etcd-lima-k8s_etcd_008.log
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        ├── kube-apiserver-lima-k8s_kube-apiserver_006.log
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        ├── kube-controller-manager-lima-k8s_kube-controller-manager_010.log
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        ├── kube-proxy-rppbc_kube-proxy_008.log
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        └── kube-scheduler-lima-k8s_kube-scheduler_010.log
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;m&#34;&gt;3&lt;/span&gt; directories, &lt;span class=&#34;m&#34;&gt;7&lt;/span&gt; files
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;protokol&lt;/code&gt; comes with a huge set of flags to alter behaviour and to target specific namespaces or pods. It is really useful in troubleshooting situations where you want to grab large parts of the cluster&amp;rsquo;s current logs, e.g. to &lt;code&gt;grep&lt;/code&gt; for certain things. It&amp;rsquo;s also quite nice in CI/CD systems where logs of pods should be downloaded as artifacts that will be stored alongside the pipeline results.&lt;/p&gt;
&lt;h2 id=&#34;tanka&#34;&gt;Tanka&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Website&lt;/em&gt;: &lt;a href=&#34;https://tanka.dev&#34;&gt;tanka.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Repository&lt;/em&gt;: &lt;a href=&#34;https://github.com/grafana/tanka&#34;&gt;github.com/grafana/tanka&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Do you remember &lt;code&gt;ksonnet&lt;/code&gt;? No? A lot of people probably don&amp;rsquo;t. The &lt;a href=&#34;https://github.com/ksonnet/ksonnet&#34;&gt;ksonnet/ksonnet&lt;/a&gt; repository was archived in September 2020, which feels like a lifetime ago. &lt;code&gt;ksonnet&lt;/code&gt; used to provide Kubernetes-specific tooling based on the &lt;a href=&#34;https://jsonnet.org&#34;&gt;jsonnet configuration language&lt;/a&gt;, which is basically a way to template and composite JSON data. The generated JSON structures can be Kubernetes objects, which can be converted to YAML or sent to the Kubernetes API directly. In essence, this was an alternative way to distribute your Kubernetes manifests with configuration options.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ksonnet&lt;/code&gt; ceased development but Grafana decided to revive the idea with &lt;code&gt;tanka&lt;/code&gt;, which was really nice. The unfortunate truth is that jsonnet is very niche, so niche that the syntax highlighting for my blog doesn&amp;rsquo;t even &lt;a href=&#34;https://github.com/alecthomas/chroma#supported-languages&#34;&gt;support&lt;/a&gt; it. The only major project outside of Grafana that seems to use jsonnet is &lt;a href=&#34;https://github.com/prometheus-operator/kube-prometheus&#34;&gt;kube-prometheus&lt;/a&gt; (which doesn&amp;rsquo;t use &lt;code&gt;tanka&lt;/code&gt;, unfortunately).&lt;/p&gt;
&lt;p&gt;I personally find the syntax of it great though, much better than Helm doing string templating on YAML. See below for a jsonnet snippet that generates a full &lt;code&gt;Deployment&lt;/code&gt; object:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;local&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;k&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;k.libsonnet&amp;#34;&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;err&#34;&gt;grafana:&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;k.apps.v1.deployment.new(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;err&#34;&gt;name=&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;&amp;#34;grafana&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;err&#34;&gt;replicas=1,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;err&#34;&gt;containers=[k.core.v1.container.new(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;err&#34;&gt;name=&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;&amp;#34;grafana&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;err&#34;&gt;image=&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;&amp;#34;grafana/grafana&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;err&#34;&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;err&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You might feel some resistance to introducing &lt;code&gt;tanka&lt;/code&gt; to your workplace because it has a learning curve, but once it clicks you never want to go back to &lt;code&gt;helm&lt;/code&gt;. &lt;em&gt;If&lt;/em&gt; you get buy-in from your colleagues this might be a huge win &amp;ndash; The ability to provide standardized libraries to generate manifests can be extremely helpful in providing a consistent baseline to teams. So it might be worth trying it out for your next project.&lt;/p&gt;
&lt;h2 id=&#34;stalk&#34;&gt;stalk&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Repository&lt;/em&gt;: &lt;a href=&#34;https://github.com/xrstf/stalk&#34;&gt;github.com/xrstf/stalk&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Another tool made by &lt;code&gt;xrstf&lt;/code&gt;! &lt;code&gt;stalk&lt;/code&gt; allows you to observe the changes in Kubernetes resources over time. This can be very useful if you just can&amp;rsquo;t understand what is happening to your &lt;code&gt;Deployment&lt;/code&gt; (or any other resource) if you struggle to observe changes when running &lt;code&gt;kubectl get&lt;/code&gt;. Usually, this is most needed when your Kubernetes controller goes into a reconciling loop. &lt;code&gt;stalk&lt;/code&gt; to the rescue - it will show &lt;code&gt;diff&lt;/code&gt; formatted output with timestamps when changes happen.&lt;/p&gt;
&lt;p&gt;In the example below, the observed changes are limited to the &lt;code&gt;.spec&lt;/code&gt; field of a &lt;code&gt;Deployment&lt;/code&gt;. So &lt;code&gt;stalk&lt;/code&gt; will start by showing &lt;code&gt;.spec&lt;/code&gt; at start time, and then log any changes it observes over time (in the example, the &lt;code&gt;Deployment&lt;/code&gt; has been scaled down to one replica later on):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ stalk deployment sample-app -s spec
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;--- (none)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;+++ Deployment default/sample-app v371701 (2024-01-27T12:43:10+01:00) (gen. 2)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gu&#34;&gt;@@ -0 +1,30 @@
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gu&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;+spec:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+  progressDeadlineSeconds: 600
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+  replicas: 2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+  revisionHistoryLimit: 10
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+  selector:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+    matchLabels:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+      app: sample-app
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+  strategy:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+    rollingUpdate:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+      maxSurge: 25%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+      maxUnavailable: 25%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+    type: RollingUpdate
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+  template:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+    metadata:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+      creationTimestamp: null
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+      labels:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+        app: sample-app
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+    spec:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+      containers:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+      - image: quay.io/embik/sample-app:latest-arm
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+        imagePullPolicy: IfNotPresent
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+        name: sample-app
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+        resources: {}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+        terminationMessagePath: /dev/termination-log
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+        terminationMessagePolicy: File
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+      dnsPolicy: ClusterFirst
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+      restartPolicy: Always
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+      schedulerName: default-scheduler
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+      securityContext: {}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+      terminationGracePeriodSeconds: 30
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;--- Deployment default/sample-app v371701 (2024-01-27T12:43:10+01:00) (gen. 2)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;+++ Deployment default/sample-app v371736 (2024-01-27T12:43:13+01:00) (gen. 3)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gu&#34;&gt;@@ -1,6 +1,6 @@
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gu&#34;&gt;&lt;/span&gt; spec:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   progressDeadlineSeconds: 600
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;-  replicas: 2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;+  replicas: 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;&lt;/span&gt;   revisionHistoryLimit: 10
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   selector:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;     matchLabels:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No more head scratching when two controllers compete on specific fields and update them several times a second.&lt;/p&gt;
&lt;h2 id=&#34;inspektor-gadget&#34;&gt;Inspektor Gadget&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Website&lt;/em&gt;: &lt;a href=&#34;https://www.inspektor-gadget.io&#34;&gt;inspektor-gadget.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Repository&lt;/em&gt;: &lt;a href=&#34;https://github.com/inspektor-gadget/inspektor-gadget&#34;&gt;github.com/inspektor-gadget/inspektor-gadget&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the tools in this blog form a toolbox, Inspektor Gadget is the toolbox &lt;em&gt;in&lt;/em&gt; the toolbox. For someone with a sysadmin background (like me) this is a treasure trove when troubleshooting low-level issues. The various small tools in Inspektor Gadget are called &amp;ndash; unsurprisingly &amp;ndash; gadgets and are based on eBPF. You can even write your own gadgets!&lt;/p&gt;
&lt;p&gt;Inspektor Gadget consists of a client component (which is a &lt;code&gt;kubectl&lt;/code&gt; plugin) and a server component, which runs as &lt;code&gt;DaemonSet&lt;/code&gt; on each Kubernetes node (after installing it).&lt;/p&gt;
&lt;p&gt;In its essence gadgets give you access to system data you could also fetch from a Kubernetes node&amp;rsquo;s shell via SSH, but Inspektor Gadget allows to fetch and process this data with the context of containers and across nodes. To just show two of the many available gadgets, below is a snapshot of active sockets in all pods in the current namespace (which usually would be much more):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ kubectl gadget snapshot socket
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;K8S.NODE                 K8S.NAMESPACE            K8S.POD                  PROTOCOL SRC                            DST                            STATUS
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;lima-k8s                 default                  sample-app-6…bf695-4cv89 TCP      r/:::8080                      r/:::0                         LISTEN
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The tracing gadgets are also amazing to understand what is actually happening in pods over time. If you want to see which DNS requests and responses happen, you can just use the &lt;code&gt;trace dns&lt;/code&gt; gadget:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ kubectl gadget trace dns
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;K8S.NODE             K8S.NAMESPACE        K8S.POD              PID         TID         COMM       QR TYPE      QTYPE      NAME                RCODE      NUMA…
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;lima-k8s             default              sample-app…695-4cv89 &lt;span class=&#34;m&#34;&gt;98533&lt;/span&gt;       &lt;span class=&#34;m&#34;&gt;98533&lt;/span&gt;       ping       Q  OUTGOING  A          google.com.                    &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;lima-k8s             default              sample-app…695-4cv89 &lt;span class=&#34;m&#34;&gt;98533&lt;/span&gt;       &lt;span class=&#34;m&#34;&gt;98533&lt;/span&gt;       ping       Q  OUTGOING  AAAA       google.com.                    &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;lima-k8s             default              sample-app…695-4cv89 &lt;span class=&#34;m&#34;&gt;98533&lt;/span&gt;       &lt;span class=&#34;m&#34;&gt;98533&lt;/span&gt;       ping       R  HOST      A          google.com.         NoError    &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;lima-k8s             default              sample-app…695-4cv89 &lt;span class=&#34;m&#34;&gt;98533&lt;/span&gt;       &lt;span class=&#34;m&#34;&gt;98533&lt;/span&gt;       ping       R  HOST      AAAA       google.com.         NoError    &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Seriously, it&amp;rsquo;s impossible to overstate how much information in an ongoing incident or a situational analysis can be discovered with Inspektor Gadget. If you operate Kubernetes clusters it should be in your go-to toolbox.&lt;/p&gt;
&lt;h2 id=&#34;skopeo&#34;&gt;skopeo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Repository&lt;/em&gt;: &lt;a href=&#34;https://github.com/containers/skopeo&#34;&gt;github.com/containers/skopeo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This one has the most GitHub stars on the list so it is statistically the tool most people are familiar with, but it still made sense to include it on the list due to its sheer usefulness.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;skopeo&lt;/code&gt; is strictly speaking not a tool for Kubernetes either &amp;ndash; it&amp;rsquo;s for interacting with container images without the need for a fully blown container runtime running (which is extremely useful on systems that don&amp;rsquo;t run &lt;code&gt;docker&lt;/code&gt; natively, like macOS or Windows). It can assist in both discovering image metadata or manipulating images in various ways.&lt;/p&gt;
&lt;p&gt;The two frequent options in daily workflows are likely &lt;code&gt;skopeo copy&lt;/code&gt; and &lt;code&gt;skopeo inspect&lt;/code&gt;. Here&amp;rsquo;s an example of inspecting the metadata of an image in a remote registry:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ skopeo inspect docker://quay.io/embik/sample-app:v0.1.0
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;Name&amp;#34;&lt;/span&gt;: &lt;span class=&#34;s2&#34;&gt;&amp;#34;quay.io/embik/sample-app&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;Digest&amp;#34;&lt;/span&gt;: &lt;span class=&#34;s2&#34;&gt;&amp;#34;sha256:efbbf29b92bd8fca3e751c1070ba5bf0f2af31983bfc9b007c7bf26681c59b4c&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;RepoTags&amp;#34;&lt;/span&gt;: &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;v0.1.0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;Created&amp;#34;&lt;/span&gt;: &lt;span class=&#34;s2&#34;&gt;&amp;#34;2023-04-07T11:38:28.791201794Z&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;DockerVersion&amp;#34;&lt;/span&gt;: &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;Labels&amp;#34;&lt;/span&gt;: &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;maintainer&amp;#34;&lt;/span&gt;: &lt;span class=&#34;s2&#34;&gt;&amp;#34;marvin@kubermatic.com&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;Architecture&amp;#34;&lt;/span&gt;: &lt;span class=&#34;s2&#34;&gt;&amp;#34;amd64&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;Os&amp;#34;&lt;/span&gt;: &lt;span class=&#34;s2&#34;&gt;&amp;#34;linux&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;Layers&amp;#34;&lt;/span&gt;: &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;sha256:91d30c5bc19582de1415b18f1ec5bcbf52a558b62cf6cc201c9669df9f748c22&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;sha256:565a1b6d716dd3c4fdf123298b33e1b3e87525cff1bdb0da54c47f70cb427727&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;LayersData&amp;#34;&lt;/span&gt;: &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;s2&#34;&gt;&amp;#34;MIMEType&amp;#34;&lt;/span&gt;: &lt;span class=&#34;s2&#34;&gt;&amp;#34;application/vnd.oci.image.layer.v1.tar+gzip&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;s2&#34;&gt;&amp;#34;Digest&amp;#34;&lt;/span&gt;: &lt;span class=&#34;s2&#34;&gt;&amp;#34;sha256:91d30c5bc19582de1415b18f1ec5bcbf52a558b62cf6cc201c9669df9f748c22&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;s2&#34;&gt;&amp;#34;Size&amp;#34;&lt;/span&gt;: 2807803,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;s2&#34;&gt;&amp;#34;Annotations&amp;#34;&lt;/span&gt;: null
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;s2&#34;&gt;&amp;#34;MIMEType&amp;#34;&lt;/span&gt;: &lt;span class=&#34;s2&#34;&gt;&amp;#34;application/vnd.oci.image.layer.v1.tar+gzip&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;s2&#34;&gt;&amp;#34;Digest&amp;#34;&lt;/span&gt;: &lt;span class=&#34;s2&#34;&gt;&amp;#34;sha256:565a1b6d716dd3c4fdf123298b33e1b3e87525cff1bdb0da54c47f70cb427727&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;s2&#34;&gt;&amp;#34;Size&amp;#34;&lt;/span&gt;: 3189012,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;s2&#34;&gt;&amp;#34;Annotations&amp;#34;&lt;/span&gt;: null
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;Env&amp;#34;&lt;/span&gt;: &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;fubectl&#34;&gt;fubectl&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Repository&lt;/em&gt;: &lt;a href=&#34;https://github.com/kubermatic/fubectl&#34;&gt;github.com/kubermatic/fubectl&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;fubectl&lt;/code&gt; is a collection of handy aliases for your shell so you don&amp;rsquo;t have to type out &lt;code&gt;kubectl&lt;/code&gt; commands all the time. While this is a project hosted by my current employer, I&amp;rsquo;ve been using it since before joining Kubermatic.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fubectl&lt;/code&gt; is a bit hard to show off in a blog post &amp;ndash; the repository README does a much better job at that. Besides the obvious aliases (&lt;code&gt;k&lt;/code&gt; instead of &lt;code&gt;kubectl&lt;/code&gt;, &lt;code&gt;kall&lt;/code&gt; instead of &lt;code&gt;kubectl get pods -A&lt;/code&gt;, etc) it does a great job at integrating fuzzy finding via &lt;a href=&#34;https://github.com/junegunn/fzf&#34;&gt;fzf&lt;/a&gt;. It makes interacting with Kubernetes much more &lt;em&gt;interactive&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;It is much easier to get logs for a pod by running &lt;code&gt;klog&lt;/code&gt; and then searching for the pod by typing fragments of its name, and then going through the second stage of selecting the right container within that pod. In a similar fashion, &lt;code&gt;kcns&lt;/code&gt; and &lt;code&gt;kcs&lt;/code&gt; help switching between contexts and namespaces without much friction.&lt;/p&gt;
&lt;p&gt;Once the various aliases of &lt;code&gt;fubectl&lt;/code&gt; are in your muscle memory, you can never got back to running &lt;code&gt;kubectl config get-contexts&lt;/code&gt; and &lt;code&gt;kubectl config use-context &amp;lt;context&amp;gt;&lt;/code&gt; instead of &lt;code&gt;kcs&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;kube-apininja&#34;&gt;kube-api.ninja&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Website&lt;/em&gt;: &lt;a href=&#34;https://kube-api.ninja&#34;&gt;kube-api.ninja&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Repository&lt;/em&gt;: &lt;a href=&#34;https://github.com/xrstf/kube-api.ninja&#34;&gt;github.com/xrstf/kube-api.ninja&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This completes the &lt;code&gt;xrstf&lt;/code&gt; trifecta of Kubernetes tools you should know about. The difference to all other tools on this list is that this is not a command line tool but a website. It tracks Kubernetes API changes over time in an easy to read table view.&lt;/p&gt;
&lt;p&gt;When was a specific resource in a specific API version added to Kubernetes? When was it migrated to another API version? What important API changes are in a specific Kubernetes version (e.g. what APIs might need to be updated in your manifests before upgrading to this Kubernetes version)? kube-api.ninja answers all those questions and many more.&lt;/p&gt;
&lt;p&gt;For example the notable API changes for Kubernetes 1.29, showing that some resource types got removed with that version:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://marvin.beckers.dev/images/blog/2024-01-kube-api-ninja.png&#34; alt=&#34;kube-api.ninja notable changes for Kubernetes 1.29&#34;&gt;&lt;/p&gt;
&lt;p&gt;kube-api.ninja is also helpful if you are interested in the evolution of APIs. Did you know that &lt;code&gt;HorizontalPodAutoscalers&lt;/code&gt; existed as a resource type before &lt;code&gt;Deployments&lt;/code&gt;? These days the Kubernetes APIs have stabilized a bit, but I wish I had this around during the &lt;code&gt;extensions&lt;/code&gt; to &lt;code&gt;apps&lt;/code&gt; migration days.&lt;/p&gt;
&lt;h2 id=&#34;kubeconform&#34;&gt;kubeconform&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Repository&lt;/em&gt;: &lt;a href=&#34;https://github.com/yannh/kubeconform&#34;&gt;github.com/yannh/kubeconform&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The last (serious) entry on this list is &lt;code&gt;kubeconform&lt;/code&gt;, which is extremely helpful in validating your Kubernetes manifests before applying them. It works great in tandem with &lt;code&gt;helm&lt;/code&gt; by first rendering your Helm chart into YAML and then passing that to &lt;code&gt;kubeconform&lt;/code&gt; to check for semantic correctness. This is how a simple CI pipeline will much improve your Helm chart&amp;rsquo;s development process:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;helm template &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  --debug &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  name path/to/helm/chart &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; tee bundle.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# run kubeconform on template output to validate Kubernetes resources.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# the external schema-location allows us to validate resources for&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# common CRDs (e.g. cert-manager resources).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kubeconform &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  -schema-location default &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  -schema-location &lt;span class=&#34;s1&#34;&gt;&amp;#39;https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json&amp;#39;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  -strict &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  -summary &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  bundle.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will help ensure that PRs changing your Helm chart still produce semantically valid Kubernetes resources, not just valid YAML.&lt;/p&gt;
&lt;h2 id=&#34;kubeconfig-bikeshed&#34;&gt;kubeconfig-bikeshed&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Repository&lt;/em&gt;: &lt;a href=&#34;https://github.com/embik/kubeconfig-bikeshed&#34;&gt;github.com/embik/kubeconfig-bikeshed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Okay, okay, okay. This one is shameless self-promotion, so I&amp;rsquo;ll keep it short. If you struggle with juggling access to many Kubernetes clusters and you feel like multiple contexts in your kubeconfig no longer cut it, kubeconfig-bikeshed (&lt;code&gt;kbs&lt;/code&gt;) might be for you. I&amp;rsquo;ve &lt;a href=&#34;https://marvin.beckers.dev/blog/bikeshedding-kubeconfig-management/&#34;&gt;started writing it&lt;/a&gt; to replace my various shell snippets that I was using to manage access to Kubernetes clusters.&lt;/p&gt;
&lt;p&gt;How many of the listed tools did you know already? Hopefully you found some new things to try out in your next troubleshooting session or CI/CD pipeline design.&lt;/p&gt;
</description>
    </item>
    
    
    
    <item>
      <title>Bikeshedding Kubeconfig Management</title>
      <link>https://marvin.beckers.dev/blog/bikeshedding-kubeconfig-management/</link>
      <pubDate>Tue, 21 Nov 2023 00:00:00 +0000</pubDate>
      
      <guid>https://marvin.beckers.dev/blog/bikeshedding-kubeconfig-management/</guid>
      <description>&lt;p&gt;When working on a platform like the &lt;a href=&#34;https://github.com/kubermatic/kubermatic&#34;&gt;Kubermatic Kubernetes Platform (KKP)&lt;/a&gt;, plenty of kubeconfigs end up in your home directory. Some of them long-lived, some already outdated 30 minutes later.&lt;/p&gt;
&lt;p&gt;Previously, I have been handling this with a couple of shell aliases and an occasional &lt;code&gt;rm -rf ~/Downloads/kubeconfig-*&lt;/code&gt;. But I would not be a software engineer if I hadn&amp;rsquo;t been obsessed with optimizing my personal workflows. There is a good reason the profession likes to &lt;a href=&#34;https://en.wiktionary.org/wiki/bikeshed&#34;&gt;bikeshed&lt;/a&gt; some things to death. And we like to have, uhm, &amp;hellip; &lt;a href=&#34;https://xkcd.com/1172/&#34;&gt;special workflows&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Since I am no exception to all of that, I decided to write my own application to manage kubeconfigs. Enter &lt;a href=&#34;https://github.com/embik/kubeconfig-bikeshed&#34;&gt;kubeconfig-bikeshed&lt;/a&gt; &amp;ndash; shorthand &lt;code&gt;kbs&lt;/code&gt;. &lt;em&gt;My&lt;/em&gt; way of keeping my &lt;del&gt;bikes&lt;/del&gt; kubeconfigs organized and out of the rain (so they don&amp;rsquo;t &lt;em&gt;rust&lt;/em&gt; &amp;ndash; you want to guess what programming language it is written in?)&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s take a look at &lt;code&gt;kbs&lt;/code&gt; in its current state, where I want it to go and &lt;em&gt;why&lt;/em&gt; I want to work on it. Right now, the project is in its infancy, but I hope to make it actually useful in the future.&lt;/p&gt;
&lt;h2 id=&#34;what-is-kbs&#34;&gt;What is kbs?&lt;/h2&gt;
&lt;p&gt;The initial v0.1 version comes with admittely a very slim feature set. There are essentially three commands in &lt;code&gt;kbs&lt;/code&gt; (ignoring help and shell completion related ones):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kbs import&lt;/code&gt; takes the path to a kubeconfig anywhere on your system (for example in &lt;code&gt;~/Downloads&lt;/code&gt;) and &lt;em&gt;copies&lt;/em&gt; it to a central &amp;ldquo;store&amp;rdquo; that &lt;code&gt;kbs&lt;/code&gt; is using. That store is in &lt;code&gt;~/.config/kbs&lt;/code&gt; by default, but respects &lt;code&gt;XDG_CONFIG_HOME&lt;/code&gt;. While importing, the kubeconfig &lt;em&gt;and&lt;/em&gt; context names get overridden with the server name. This is one of the things I kept doing when downloading kubeconfigs, and I often found the FQDN that is hosting the Kubernetes API to be the most &amp;ldquo;accurate&amp;rdquo; descriptor of a kubeconfig. If that is not suitable, the &lt;code&gt;--name&lt;/code&gt; flag allows to set a name explicitly.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kbs list&lt;/code&gt; lists the available kubeconfigs from the &amp;ldquo;store&amp;rdquo;. It is not very useful on its own, but mostly for scripting purposes and in combination with the next command.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kbs use&lt;/code&gt;, which prints a shell snippet to export a kubeconfig from the store by name to the &lt;code&gt;KUBECONFIG&lt;/code&gt; environment variable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At the moment, the tool is restricted to import &amp;ldquo;simple&amp;rdquo; kubeconfigs, which means they can only have one cluster and one user entry. In the future I hope to improve on this.&lt;/p&gt;
&lt;p&gt;This is the baseline for my tool, and thus the v0.1 release. In coming releases, I want to add a boatload of features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;flags to change behavior of commands (e.g. &lt;code&gt;--short&lt;/code&gt; on &lt;code&gt;kbs import&lt;/code&gt; to just use the first portion of the server FQDN as name).&lt;/li&gt;
&lt;li&gt;let &lt;code&gt;kbs use&lt;/code&gt; remember the last active kubeconfig and add &lt;code&gt;kbs use -&lt;/code&gt; to &amp;ldquo;restore&amp;rdquo; the last selected kubeconfig.&lt;/li&gt;
&lt;li&gt;add a command to fetch kubeconfigs from remote systems (e.g. for a &lt;a href=&#34;https://docs.kubermatic.com/kubermatic/v2.24/architecture/#user-cluster&#34;&gt;user cluster&lt;/a&gt; in KKP).&lt;/li&gt;
&lt;li&gt;add a command to &amp;ldquo;prune&amp;rdquo; outdated kubeconfigs. The semantics of that are still not fully settled in my head, but I can see it trying to establish a HTTPS connection to the Kubernetes API to decide if the server still exists.&lt;/li&gt;
&lt;li&gt;a way to manually clean up kubeconfigs (without going to &lt;code&gt;~/.config/kbs&lt;/code&gt; manually).&lt;/li&gt;
&lt;li&gt;perhaps a way to attach metadata to kubeconfigs when importing them. This could be useful to categorize and filter kubeconfigs, e.g. exclude any kubeconfig that has a &lt;code&gt;vpn=internal&lt;/code&gt; flag from being pruned.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While writing this section, new ideas come up every second. There is a lot of potential in &lt;code&gt;kbs&lt;/code&gt; that I hope to explore.&lt;/p&gt;
&lt;h3 id=&#34;installation&#34;&gt;Installation&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;kbs&lt;/code&gt; can be installed via &lt;code&gt;brew&lt;/code&gt; (if you have Homebrew) or &lt;code&gt;cargo&lt;/code&gt; (if you are on Linux and have a working Rust toolchain), depending on what system you are on. For &lt;code&gt;brew&lt;/code&gt;, installation is as simple as:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ brew tap embik/tap
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ brew install kubeconfig-bikeshed
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Getting the tap up and running was relatively simple, but writing the formula for a Rust binary was surprisingly difficult to get right. When you search for that online, you run into tons of outdated advice. Maybe this should be the topic for another blog post once I feel more comfortable with it.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;cargo&lt;/code&gt;, it is even simpler after pushing my first crate to &lt;a href=&#34;https://crates.io&#34;&gt;crates.io&lt;/a&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ cargo install kubeconfig-bikeshed
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After installation, I highly recommend to set up shell integration. For example for zsh, add the following snippet to your &lt;code&gt;.zshrc&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# load kubeconfig-bikeshed shell completion &amp;amp; magic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;command&lt;/span&gt; -v kbs &lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;&amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;source&lt;/span&gt; &amp;lt;&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;kbs shell completion zsh&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;source&lt;/span&gt; &amp;lt;&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;kbs shell magic zsh&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But wait &amp;hellip; what is &lt;code&gt;kbs shell magic zsh&lt;/code&gt;?&lt;/p&gt;
&lt;h3 id=&#34;shell-magic&#34;&gt;Shell magic&lt;/h3&gt;
&lt;p&gt;One of the more interesting bits in &lt;code&gt;kbs&lt;/code&gt; is the optional shell &amp;ldquo;magic&amp;rdquo;. It allows the user to run &lt;code&gt;kbs&lt;/code&gt; to select a kubeconfig and set it for further use with e.g. &lt;code&gt;kubectl&lt;/code&gt;. For zsh, it is loaded in the snippet above. For bash, it can be loaded via &lt;code&gt;kbs shell magic bash&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As with all good magic, the actual trick is very simple. This defines a function called &lt;code&gt;kbs&lt;/code&gt; in bash:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;alias&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;_inline_fzf&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;fzf --height=50% --reverse -0 --inline-info --border&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;alias&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;_kbs_bin&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;type&lt;/span&gt; -p kbs&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; kbs&lt;span class=&#34;o&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$#&lt;/span&gt; -eq &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;# if no parameters are passed, we want to run fzf on available kubeconfigs and set the selected one as active kubeconfig&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nb&#34;&gt;eval&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;_kbs_bin use &lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;_kbs_bin ls &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; _inline_fzf&lt;span class=&#34;k&#34;&gt;))&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;# if parameters are passed, we just call the kbs binary directly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;_kbs_bin&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$@&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This acts like a shim between the user and the actual &lt;code&gt;kbs&lt;/code&gt; binary. This is necessary for actually setting the &lt;code&gt;KUBECONFIG&lt;/code&gt; environment variable, which feels like a core functionality of any good kubeconfig manager. You cannot set environment variables from within an application, so this function runs &lt;code&gt;kbs ls&lt;/code&gt; to list available kubeconfigs, pipes them through &lt;code&gt;fzf&lt;/code&gt; so the user can interactively search and select one, and then exports the full path as &lt;code&gt;KUBECONFIG&lt;/code&gt; (&lt;code&gt;kbs use&lt;/code&gt; prints something like &lt;code&gt;export KUBECONFIG=path/to/kubeconfig&lt;/code&gt; that is going through &lt;code&gt;eval&lt;/code&gt; and applied to the current shell).&lt;/p&gt;
&lt;p&gt;This only happens when &lt;code&gt;kbs&lt;/code&gt; (the shell function now) is called without arguments. if it is called with any arguments, they are passed through to the &lt;code&gt;kbs&lt;/code&gt; &lt;em&gt;binary&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The neat trick I learned while working on this is the use of proper shell built-ins. &lt;code&gt;which&lt;/code&gt; is the much more popular command to determine where and if a command exists. However, since this is overriding the &lt;code&gt;kbs&lt;/code&gt; command with a function, it needs to know where the &lt;code&gt;kbs&lt;/code&gt; &lt;strong&gt;binary&lt;/strong&gt; is. &lt;code&gt;type -p&lt;/code&gt; does exactly that by simply looking for binaries, and not checking the shell itself.&lt;/p&gt;
&lt;p&gt;Note that while &lt;code&gt;type&lt;/code&gt; is a unix standard built-in, it works differently across shells. The zsh magic for example uses &lt;code&gt;whence&lt;/code&gt; instead of &lt;code&gt;type&lt;/code&gt;, because &lt;code&gt;type&lt;/code&gt; prints not only the path:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ &lt;span class=&#34;nb&#34;&gt;type&lt;/span&gt; -p kbs
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kbs is /opt/homebrew/bin/kbs
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Since shell magic is per shell, I can get away with not using &lt;code&gt;type&lt;/code&gt; where it doesn&amp;rsquo;t suit what I need.&lt;/p&gt;
&lt;h2 id=&#34;why-write-your-own&#34;&gt;Why write your own&lt;/h2&gt;
&lt;p&gt;Because it is fun. No, seriously. Working in an established ecosystem like Cloud Native can be a challenge sometimes, even though it is a relatively young space. Do not see this as a complaint &amp;ndash; Collaboration, discussions, guard rails and architecture considerations are vital to good software. And I enjoy being involved in them.&lt;/p&gt;
&lt;p&gt;But writing my own tools can be refreshing. They can do &lt;em&gt;exactly&lt;/em&gt; what I want them to do, scratching that super specific itch I have. If people feel the same way I do about those itches, they are very welcome to use &lt;code&gt;kbs&lt;/code&gt;. If they feel it &lt;em&gt;doesn&amp;rsquo;t&lt;/em&gt; scratch &lt;em&gt;their&lt;/em&gt; itch, they can use another tool or write their own. Or fork &lt;code&gt;kbs&lt;/code&gt;. Finding a personal workflow that &lt;em&gt;works&lt;/em&gt; has many ways.&lt;/p&gt;
&lt;p&gt;Our industry is split on &amp;ldquo;Not Invented Here&amp;rdquo; (NIH) syndrome. Some organizations extensively suffer from it, while others are avoiding it &amp;ndash; with mostly good reasons &amp;ndash; like the plague. I believe collaboration between people with diverse backgrounds and corporate culture always produces the best outcome and should be the default choice over building it on your own. But I think if I can get away with NIH anywhere, it is tools used in personal workflows.&lt;/p&gt;
&lt;h3 id=&#34;learning-by-doing&#34;&gt;Learning by doing&lt;/h3&gt;
&lt;p&gt;The way I learn &amp;ndash; and I assume most do &amp;ndash; is by &lt;em&gt;doing&lt;/em&gt;. Rust is one of &lt;em&gt;those&lt;/em&gt; programming languages. The ones I have been aware of for years, but never really got into over all those years. The first time I tried to get into Rust was with &lt;a href=&#34;https://github.com/embik/nightshift&#34;&gt;nightshift&lt;/a&gt;, a tool to reduce blue light emissions when using &lt;a href=&#34;https://swaywm.org&#34;&gt;sway&lt;/a&gt; as your window manager on Linux. That repository was created six years ago! And it never really got anywhere because I struggled with the language a lot. Since then I have used Rust for a small tool called &lt;a href=&#34;https://github.com/embik/ing-csv-importer&#34;&gt;ing-csv-importer&lt;/a&gt; which frankly speaking could have been a shell script.&lt;/p&gt;
&lt;p&gt;But I still wanted to learn Rust! And I was bothered by my kubeconfig (mis-)management. The obvious solution to both problems was to combine them. Since I had a rough idea of what the tool should be capable of, the Rust learning process was much more guided. I knew what to look for. The code itself is probably terrible to experienced Rustaceans, but it works and I have already learned a lot about Rust from it. I hope to refine it in the future.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kbs&lt;/code&gt; is a &amp;ldquo;low impact&amp;rdquo; learning project. I had the idea back in May of 2023. Set up the repository, pulled in some dependencies and called it a day. But since there was no &lt;em&gt;pressure&lt;/em&gt; to come back to it, I was able to take my time and get back into it once I was motivated. No one cared if I delivered &amp;ldquo;on time&amp;rdquo;, and so I was able to get back into it half a year later.&lt;/p&gt;
&lt;p&gt;And after a couple of days of ramp up, I have to say I am delighted with Rust. The language has a certain elegance to it that I cannot explain. But I enjoy concepts like &lt;code&gt;match&lt;/code&gt; that make for really nice code. Consider for example the snippet below, which determines the &lt;code&gt;name&lt;/code&gt; variable used for a kubeconfig and its context depending on whether the &lt;code&gt;--name&lt;/code&gt; flag is set:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;: &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;match&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;matches&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get_one&lt;/span&gt;::&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;Some&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;clone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hostname&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;kubeconfig&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;get_hostname&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;kubeconfig&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;url&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Url&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;parse&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hostname&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;as_str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;host&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;url&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;host_str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ok_or&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;failed to parse host from server URL&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;host&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to_string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This feels so much less clunky to me than an &lt;code&gt;if&lt;/code&gt; block in Go, for example. Is that &amp;ldquo;factually&amp;rdquo; correct? Maybe not. But Rust has a lot of concepts that I never knew I missed from other languages and I expect to do more work with it in the future.&lt;/p&gt;
&lt;h2 id=&#34;closing-thoughts&#34;&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Does &lt;code&gt;kbs&lt;/code&gt; do something special? Absolutely not. If people are looking for a mature kubeconfig management solution, they are &amp;ndash; at the moment, and maybe forever &amp;ndash; better off with &lt;a href=&#34;https://github.com/SimonTheLeg/konf-go&#34;&gt;konf-go&lt;/a&gt;. But the work on this is a lot of fun and gives a certain feeling of self-empowerement once I had &amp;ldquo;finished&amp;rdquo; the v0.1.0 release end-to-end (from first commit to available Homebrew package).&lt;/p&gt;
&lt;p&gt;Now I am excited to integrate &lt;code&gt;kbs&lt;/code&gt; into my daily workflow and improve it where necessary. I plan to maintain it as long as I am working with masses of kubeconfigs, which I sincerely hope won&amp;rsquo;t change in the near future.&lt;/p&gt;
&lt;p&gt;Anyways, I wish some people find use in &lt;code&gt;kbs&lt;/code&gt; or the inspiration to start their own learning projects (or not; enjoy your free time with whatever makes you happy). Oh, and regarding that list of programming languages I want to get into &amp;ndash; I&amp;rsquo;m coming for you next, &lt;a href=&#34;https://elixir-lang.org&#34;&gt;Elixir&lt;/a&gt; and/or &lt;a href=&#34;https://gleam.run&#34;&gt;Gleam&lt;/a&gt; (once I get &lt;code&gt;kbs&lt;/code&gt; where I want it to be, but &amp;hellip; details, am I right?)&lt;/p&gt;
</description>
    </item>
    
    
    
    <item>
      <title>Running a Go Debugger in Kubernetes</title>
      <link>https://marvin.beckers.dev/blog/running-a-go-debugger-in-kubernetes/</link>
      <pubDate>Sun, 08 Oct 2023 00:00:00 +0000</pubDate>
      
      <guid>https://marvin.beckers.dev/blog/running-a-go-debugger-in-kubernetes/</guid>
      <description>&lt;p&gt;&lt;em&gt;(Note: This blog post is a written version of my talk &lt;a href=&#34;https://github.com/embik/ephemeral-debugger-talk&#34;&gt;Ephemeral Containers in Action - Running a Go Debugger in Kubernetes&lt;/a&gt;. Slides and recordings are available in the linked repository.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Identifying bugs in our code can be hard; really hard. In complex microservice environments, it can be even harder, since reproducing the exact request flow which triggered a bug can be quite the challenge. Thankfully the modern observability stack can be quite helpful in the matter.&lt;/p&gt;
&lt;p&gt;But sometimes extended observability might not be available &lt;em&gt;or&lt;/em&gt; it&amp;rsquo;s still not enough to identify the problematic code path. In those cases I like to break out a proper &lt;a href=&#34;https://en.wikipedia.org/wiki/Debugger&#34;&gt;debugger&lt;/a&gt;, set specific breakpoints and investigate the program&amp;rsquo;s state at those breakpoints.&lt;/p&gt;
&lt;p&gt;But how do we run a debugger &lt;em&gt;within&lt;/em&gt; Kubernetes, where our application actually lives? We do not want to run our application with active debugging all the time! Not a problem (anymore)! We can utilize ephemeral containers. It will allow us to launch a debugger into an existing Pod &lt;em&gt;on demand&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Before going into the details, let&amp;rsquo;s review what debugging actually is and what considerations we need to be aware of when debugging Go applications.&lt;/p&gt;
&lt;h2 id=&#34;basics-of-debugging-go&#34;&gt;Basics of debugging (Go)&lt;/h2&gt;
&lt;p&gt;Debugging is a long-standing staple of software development. If it is not part of &lt;em&gt;our&lt;/em&gt; development toolbox yet, it might be worth looking into it. &lt;code&gt;print&lt;/code&gt; based debugging sometimes has lower friction, but knowing how to use a debugger is a powerful skill.&lt;/p&gt;
&lt;p&gt;For Go, the go-to debugger is, without a doubt, &lt;a href=&#34;https://github.com/go-delve/delve&#34;&gt;Delve&lt;/a&gt;. Delve is a debugger specific to Go and has been around for years. While &lt;a href=&#34;https://go.dev/doc/gdb&#34;&gt;using GBD is possible&lt;/a&gt;, the official Go documentation recommends using Delve when possible. For some basics of debugging Go applications with Delve, there is &lt;a href=&#34;https://golang.cafe/blog/golang-debugging-with-delve.html&#34;&gt;an article available from golang.cafe&lt;/a&gt; that serves as a good introduction.&lt;/p&gt;
&lt;p&gt;The gist however is that debuggers allow to inspect application state during code execution. We can set breakpoints and look at variable contents when that breakpoint is hit. In Go, specifically, it is also possible to inspect Goroutines.&lt;/p&gt;
&lt;p&gt;This means debuggers are incredibly powerful in identifying faults in code since the whole state can be inspected at any given time. Once set up, effective debugger usage beats &lt;code&gt;print&lt;/code&gt;-style debugging by a mile or two.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s take a look at the requirements to run a debugger.&lt;/p&gt;
&lt;h3 id=&#34;linux-capabilities&#34;&gt;Linux capabilities&lt;/h3&gt;
&lt;p&gt;While interacting with the operating system, we often interact with kernel APIs. For this article we will take a quick look at the Linux kernel in specific, but similar concepts probably exist in the operating system of our choice. They do not overlap 100%, but they are probably close enough.&lt;/p&gt;
&lt;p&gt;In Linux, there is a concept called &lt;a href=&#34;https://man7.org/linux/man-pages/man7/capabilities.7.html&#34;&gt;capabilities&lt;/a&gt;. Capabilities are assigned to a process and allow to interact with certain functionality of the kernel. This enables more granular permissions than running a process as a super user.&lt;/p&gt;
&lt;p&gt;For debuggers, a specific capability is usually required - &lt;code&gt;CAP_SYS_PTRACE&lt;/code&gt; - because it unlocks calling the &lt;code&gt;ptrace&lt;/code&gt; system call. Debuggers need this to attach to running processes and to interact with their memory and registers. It essentially allows us to &amp;ldquo;take control&amp;rdquo; of an already running process.&lt;/p&gt;
&lt;p&gt;That is a scary-looking system call and there is good reason this is locked by a capability. But we need it! As a Linux user, we might not see this level of detail (and maybe run the debugger via &lt;code&gt;sudo&lt;/code&gt;) but since we are thinking of running a debugger in Kubernetes, we need to be aware of this. A container in Kubernetes does not have this capability by default. Let&amp;rsquo;s keep this in mind, it will come up later again.&lt;/p&gt;
&lt;h3 id=&#34;go-compile-flags&#34;&gt;Go compile flags&lt;/h3&gt;
&lt;p&gt;Debuggers usually require a certain level of information encoded in the binary that they try to debug. This means that the &amp;ldquo;production&amp;rdquo; build of our application binary might not be suitable for debugging.&lt;/p&gt;
&lt;p&gt;To determine this, we can look at how our binaries are built, e.g. in a &lt;code&gt;Makefile&lt;/code&gt; or &lt;code&gt;Dockerfile&lt;/code&gt;. If the following flags are &lt;strong&gt;present&lt;/strong&gt; in &lt;code&gt;go build&lt;/code&gt;&amp;rsquo;s &lt;code&gt;-ldflags&lt;/code&gt; we definitely need a separate debug build:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-s&lt;/code&gt;: &lt;em&gt;Omit the symbol table and debug information.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-w&lt;/code&gt;: &lt;em&gt;Omit the DWARF symbol table.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both flags allow us to optimize the binary size at the expense of debug information, which is usually what we want for production builds. If this is the case, a separate image needs to be provided that does &lt;strong&gt;not&lt;/strong&gt; have those flags set. Otherwise, the application cannot be debugged. This also means that we will need to launch a separate application Pod with the debug-enabled application build. More on that later.&lt;/p&gt;
&lt;p&gt;In addition, some flags can be &lt;em&gt;helpful&lt;/em&gt; for debugging. Since we rely on line numbers to set breakpoints, we need to make sure that our code is not optimized by the compiler. This &lt;em&gt;can&lt;/em&gt; happen if the compiler recognizes code patterns that can be simplified. Some parts of the code might therefore be changed and cause issues with debugging, although &lt;a href=&#34;https://github.com/go-delve/delve/issues/2295#issuecomment-758130802&#34;&gt;optimized code is generally considered okay (with some pitfalls)&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Code optimization can be disabled if required by passing additional flags:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ go build -gcflags&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;all=-N -l&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will pass two flags to the Go compiler for all invocations. The flags being&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-N&lt;/code&gt;: &lt;em&gt;Disable optimizations.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-l&lt;/code&gt;: &lt;em&gt;Disable inlining.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We should be careful with these flags though, since the compiler usually has some pretty good ideas about optimizing code. Let&amp;rsquo;s only add them if debugging gives us unexpected results or errors when trying to set breakpoints.&lt;/p&gt;
&lt;h3 id=&#34;remote-debugging&#34;&gt;Remote debugging&lt;/h3&gt;
&lt;p&gt;Now we have established debugging as a concept - But so far, we have been talking about attaching ourselves to processes on the system. Which obviously means that we need to be on the same (virtual) machine. But if the application is running within Kubernetes, it would be nice to be able to debug from the comfort of our laptop.&lt;/p&gt;
&lt;p&gt;Thankfully, the &lt;a href=&#34;https://microsoft.github.io/debug-adapter-protocol/&#34;&gt;Debug Adapter Protocol&lt;/a&gt; exists to help us out. Developed for Visual Studio Code to have a standardized protocol to interact with so-called debug adapters (intermediate layers that translate between a client and the actual debugger), it has seen solid adoption and will help us out here (even if we do not use VS Code).&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://marvin.beckers.dev/images/blog/2023-09-running-a-go-debugger-in-kubernetes/dap_flow.png&#34; alt=&#34;Debug Adapter Protocol Flow&#34;&gt;&lt;/p&gt;
&lt;p&gt;While DAP was meant to enable debug adapters as a layer in between, its success has triggered adoption in debuggers directly, overshooting the stated project goal. Delve has a native DAP implementation and can act as a DAP server. It &lt;em&gt;also&lt;/em&gt; has its own protocol implementation that stems from before DAP adoption and can serve both protocols as a server. We will focus on DAP for this blog post since it is a (somewhat) new and emerging standard. For adaption into other language stacks, the Delve protocol is not interesting at all.&lt;/p&gt;
&lt;p&gt;The interesting bit about DAP (and Delve&amp;rsquo;s own protocol) is that it is &lt;em&gt;networked&lt;/em&gt;. This means we can suddenly traverse network boundaries when starting a debugging session! We can launch a debugging session on the system running the application (i.e. in Kubernetes) and then connect to it via a remote client (i.e. our local system).&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://marvin.beckers.dev/images/blog/2023-09-running-a-go-debugger-in-kubernetes/dlv_remote.png&#34; alt=&#34;Delve Remote Debugging&#34;&gt;&lt;/p&gt;
&lt;p&gt;We will use that to our advantage, since we prefer integrating debug sessions with our usual development tools. But to do so, we need an active debugger to connect to. Next, we will take a look at how to get the debugger (with a DAP server onboard) into Kubernetes in the first place.&lt;/p&gt;
&lt;h2 id=&#34;a-primer-on-ephemeral-containers&#34;&gt;A primer on ephemeral containers&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/workloads/pods/ephemeral-containers/&#34;&gt;Ephemeral containers&lt;/a&gt; are a feature in Kubernetes that became generally available in Kubernetes 1.25. Before ephemeral containers, there were two types of containers in a Pod specification: InitContainers and (normal) containers. InitContainers run before the &amp;ldquo;normal&amp;rdquo; containers at Pod startup to execute some initialisation logic (note this is also changing with &lt;a href=&#34;https://kubernetes.io/docs/concepts/workloads/pods/init-containers/#api-for-sidecar-containers&#34;&gt;sidecar patterns available in InitContainers&lt;/a&gt;). But both types of containers needed to be defined in the Pod specification when creating the Pod. Pods were more or less immutable, we could not change them once they were created.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://marvin.beckers.dev/images/blog/2023-09-running-a-go-debugger-in-kubernetes/pods_before_125.png&#34; alt=&#34;Container Types in Pods Before Kubernetes 1.25&#34;&gt;&lt;/p&gt;
&lt;p&gt;This changes with ephemeral containers! Ephemeral containers are part of the Pod specification but are implemented as a subresource. And most importantly the list of ephemeral containers can be amended while the Pod is running. In fact, we can only create ephemeral containers while the Pod is running since subresources cannot be created at the same time as the actual resource.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://marvin.beckers.dev/images/blog/2023-09-running-a-go-debugger-in-kubernetes/pods_after_125.png&#34; alt=&#34;Container Types in Pods After Kubernetes 1.25&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;command-line-tooling&#34;&gt;Command line tooling&lt;/h3&gt;
&lt;p&gt;Since we established that we will need some special tooling to interact with the &lt;code&gt;ephemeralContainers&lt;/code&gt; subresource on a Pod, let&amp;rsquo;s look at our options.&lt;/p&gt;
&lt;p&gt;The obvious candidate is &lt;code&gt;kubectl&lt;/code&gt;. Thankfully, it got us covered! &lt;code&gt;kubectl debug&lt;/code&gt; is available as a command in the &lt;code&gt;kubectl&lt;/code&gt; (kube-control? kube-cuttle? kube-c-t-l?) toolbox. It allows us to launch ephemeral containers into specific Pods.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ kubectl debug &amp;lt;pod&amp;gt; -it --image&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;busybox --target&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&amp;lt;container&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With the example above, an ephemeral container using the &lt;code&gt;busybox&lt;/code&gt; image (&lt;code&gt;--image&lt;/code&gt;) is attached to the specified Pod &lt;code&gt;&amp;lt;pod&amp;gt;&lt;/code&gt;. It also connects the ephemeral container to a specific &lt;code&gt;&amp;lt;container&amp;gt;&lt;/code&gt; (&lt;code&gt;--target&lt;/code&gt;), which means that we break down the isolation between containers so the ephemeral container can access the process namespace of the target one. We also tell &lt;code&gt;kubectl debug&lt;/code&gt; to drop us into an interactive shell in the ephemeral container (&lt;code&gt;-i&lt;/code&gt; and &lt;code&gt;-t&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kubectl debug&lt;/code&gt; is a very powerful toolbox for debugging in Kubernetes! If we choose to provide a special build of our application image (as discussed earlier), we cannot debug a &amp;ldquo;live&amp;rdquo; Pod. &lt;em&gt;But&lt;/em&gt; we can use &lt;code&gt;--copy-to&lt;/code&gt; and &lt;code&gt;--set-image&lt;/code&gt; to&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;create a new Pod from the &amp;ldquo;template&amp;rdquo; of a currently running Pod and&lt;/li&gt;
&lt;li&gt;override the image used for the application container to a debug-enabled image&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;which could be used to run a variant of this debug workflow against a Pod copy.&lt;/p&gt;
&lt;h4 id=&#34;debugging-profiles&#34;&gt;Debugging profiles&lt;/h4&gt;
&lt;p&gt;As established earlier, a debugger needs &lt;code&gt;CAP_SYS_PTRACE&lt;/code&gt; (essentially by definition). By default a container does not have this capability, and for good reason!&lt;/p&gt;
&lt;p&gt;But &lt;code&gt;kubectl debug&lt;/code&gt; comes with a flag called &lt;code&gt;--profile&lt;/code&gt; that helps with setting the right security configuration on the ephemeral container. This is a fairly recent addition, so not all use cases might be covered and it&amp;rsquo;s not always easy to figure out what a profile &lt;em&gt;exactly&lt;/em&gt; provides, but for us, it is sufficient.&lt;/p&gt;
&lt;p&gt;What we need is &lt;code&gt;--profile=general&lt;/code&gt;, since it provides &lt;code&gt;CAP_SYS_PTRACE&lt;/code&gt; and &lt;a href=&#34;https://github.com/kubernetes/kubernetes/blob/89a4ea3e1e4ddd7f7572286090359983e0387b2f/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles.go#L144&#34;&gt;nothing more&lt;/a&gt;. Easy peasy.&lt;/p&gt;
&lt;p&gt;As a minor side note: When I started working on this topic not everything was as polished as it is today. Because profiles were not yet available in &lt;code&gt;kubectl debug&lt;/code&gt;, I had to develop my own little &lt;code&gt;kubectl&lt;/code&gt; plugin called &lt;a href=&#34;https://github.com/embik/kubectl-ephemeral&#34;&gt;kubectl-ephemeral&lt;/a&gt;. It allows usage of the full API for the &lt;code&gt;EphemeralContainer&lt;/code&gt; type to create ephemeral containers, which might prove useful if no profile provides the necessary configuration. Usually, we want &lt;code&gt;kubectl debug&lt;/code&gt; though.&lt;/p&gt;
&lt;h2 id=&#34;delve-as-ephemeral-container&#34;&gt;Delve as ephemeral container&lt;/h2&gt;
&lt;p&gt;Now we have everything that we need in building blocks. Let&amp;rsquo;s put it together! In this case, I am debugging a &lt;a href=&#34;https://github.com/embik/ephemeral-debugger-talk/tree/main/sample-app&#34;&gt;sample application&lt;/a&gt; that is a simple Go web server. It is already running in Kubernetes. We now want to create an ephemeral container in one of the Pods for that application. The ephemeral container should start Delve, attach to the application, and open a DAP server port.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ kubectl debug &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;    --profile&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;general &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;    --target&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;sample-app &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;    --image&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;quay.io/embik/dlv:v1.20.1 &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;    sample-app-&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;...&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;    -- dlv --listen&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;127.0.0.1:2345 --headless&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;true&lt;/span&gt; attach &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There we go. We attach to process id &lt;code&gt;1&lt;/code&gt; because that is the process which started in the target container. Since we are connected to the PID namespace of the target container, we can just target the initial process - This only works if our target container does not have another entrypoint than the application binary.&lt;/p&gt;
&lt;p&gt;One last thing though: We opened the DAP server on &lt;code&gt;127.0.0.1:2345&lt;/code&gt;. That&amp;rsquo;s not accessible from outside the Pod! How are we supposed to connect our client to that? And how do we keep this port secured as DAP knows no authentication?&lt;/p&gt;
&lt;p&gt;Thankfully, &lt;code&gt;kubectl port-forward&lt;/code&gt; helps us close that last gap. It does not need a port definition in our &lt;code&gt;PodSpec&lt;/code&gt;, it works ad-hoc. Which means we can simply run&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ kubectl port-forward pod/sample-app-&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;...&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;2345&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which opens port &lt;code&gt;2345&lt;/code&gt; on our local system and forwards it to the port within our Pod that has Delve running. This is our setup visualized:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://marvin.beckers.dev/images/blog/2023-09-running-a-go-debugger-in-kubernetes/full_overview.png&#34; alt=&#34;Full Setup Overview&#34;&gt;&lt;/p&gt;
&lt;p&gt;To validate that our setup is running, try connecting to the local port with &lt;code&gt;dlv connect&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ dlv connect localhost:2345
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If everything is working, this will connect us to the remote instance of Delve running in Kubernetes. From here, we can use the interactive shell of &lt;code&gt;dlv&lt;/code&gt; to start debugging!&lt;/p&gt;
&lt;p&gt;But we started out with the promise of integration into our workflows - And while the command line might be part of our usual workflow in developing software, we can do better than that.&lt;/p&gt;
&lt;h3 id=&#34;remote-debugging-with-vs-code&#34;&gt;Remote debugging with VS Code&lt;/h3&gt;
&lt;p&gt;Since we have a working connection to the Delve headless server instance in our ephemeral container now, we can use a DAP client of our choice to connect to it. The most popular DAP client - by a mile or two, probably - is Visual Studio Code (VS Code). It also happens to be one of the more popular editors out there.&lt;/p&gt;
&lt;p&gt;To configure VS Code to talk to Delve, we can drop a &lt;code&gt;launch.json&lt;/code&gt; configuration file into the &lt;code&gt;.vscode&lt;/code&gt; directory of our project (if the folder does not exist, we need to create it). VS Code will read it as a project-specific debugging configuration.&lt;/p&gt;
&lt;p&gt;This - or something similar - is the file for my example project:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;version&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;0.2.0&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;configurations&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Remote Attach&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;go&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;request&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;attach&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;debugAdapter&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;dlv-dap&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;mode&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;remote&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;substitutePath&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nt&#34;&gt;&amp;#34;from&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;${workspaceFolder}&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nt&#34;&gt;&amp;#34;to&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;/build&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;port&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2345&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;host&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;127.0.0.1&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All of this is important to configure the connection to a running debugger, but we care mostly about &lt;code&gt;type&lt;/code&gt; being set to &lt;code&gt;&amp;quot;go&amp;quot;&lt;/code&gt; (since we are debugging Go code), &lt;code&gt;debugAdapter&lt;/code&gt; set to &lt;code&gt;&amp;quot;dlv-dap&amp;quot;&lt;/code&gt; (so VS Code knows it will talk to Delve via DAP), &lt;code&gt;port&lt;/code&gt; and &lt;code&gt;host&lt;/code&gt; referencing the local port-forwarding we have opened, and - most importantly - &lt;code&gt;substitutePath&lt;/code&gt; set correctly.&lt;/p&gt;
&lt;p&gt;Because Go embeds the full path in its debugging information (unless we trim paths), we need to tell VS Code about the original path that the binary was built in. In our case, this might have been &lt;code&gt;/build&lt;/code&gt;, the working directory set in our &lt;code&gt;Dockerfile&lt;/code&gt;. Therefore, we instruct VS Code to rewrite path information when interacting with the debugger from our current workspace folder to &lt;code&gt;/build&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Once this is done, VS Code will offer the new debugging mode called &lt;em&gt;Remote Attach&lt;/em&gt; from its debugging screen via the green play button. For a detailed overview of debugging capabilities in VS Code please &lt;a href=&#34;https://code.visualstudio.com/docs/editor/debugging&#34;&gt;check out the official documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;closing-thoughts&#34;&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Phew, that was a lot. Hopefully this overview helps with designing a personal debugging workflow in Kubernetes. This was not meant as a step by step tutorial, but more of a showcasing for what ephemeral containers can do. Maybe it can serve as basis for additional, similar workflows.&lt;/p&gt;
&lt;p&gt;I think it is clear from the full text that this is not an outright recommendation. A lot of preconditions need to be taken into consideration before a debugging session in Kubernetes will work. &lt;em&gt;If&lt;/em&gt; they can be fulfilled, Delve can really help with figuring out deeply rooted bugs in an application.&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Thank you to my personal editor for reviewing and improving this post!&lt;/small&gt;&lt;/p&gt;
</description>
    </item>
    
    
    
    <item>
      <title>Private DNS with CoreDNS, Podman and Ansible</title>
      <link>https://marvin.beckers.dev/blog/coredns-podman-private-dns-resolver/</link>
      <pubDate>Wed, 12 Aug 2020 00:00:00 +0000</pubDate>
      
      <guid>https://marvin.beckers.dev/blog/coredns-podman-private-dns-resolver/</guid>
      <description>&lt;p&gt;Running a private DNS resolver is useful in quite a few situations, for example in a home lab or on an internal company network; basically everywhere where you want to give names to private systems. &lt;a href=&#34;https://coredns.io&#34;&gt;CoreDNS&lt;/a&gt; is a simple DNS server that can be used in such a situation - if the name rings a bell with you, it&amp;rsquo;s because CoreDNS is the standard in-cluster DNS solution for Kubernetes for some time now. In practice, this means that large microservice architectures rely on it to resolve internal and external hostnames.&lt;/p&gt;
&lt;p&gt;I decided to go with CoreDNS for my setup because of that fact (it&amp;rsquo;s a tool proved to be battle-tested in Kubernetes environments I worked with) and the simplicity of configuration - it needs a single configuration file, called &lt;code&gt;Corefile&lt;/code&gt;, that defines the DNS resolver&amp;rsquo;s behavior. Moreover, CoreDNS is based on a plugin architecture, which potentially allows to extend it with custom functionality.&lt;/p&gt;
&lt;p&gt;The small network that required a private DNS resolver in my case was however too small to run Kubernetes, so it was necessary to find a different way of running it. Since it is a Go program, downloading and running a binary would probably work - But I wanted to run it in a container for better isolation. &lt;a href=&#34;https://www.centos.org&#34;&gt;CentOS 8&lt;/a&gt; added support for a new container runtime largely spearheaded by Red Hat and Fedora called &lt;a href=&#34;https://podman.io&#34;&gt;podman&lt;/a&gt;. podman is interesting because it gets rid of the omniscient (Docker) daemon, allowing for running containers in a more stand-alone manner. It also supports running containers as non-root users, albeit we won&amp;rsquo;t use this feature for now.&lt;/p&gt;
&lt;p&gt;To deploy CoreDNS and maintain its configuration, I chose &lt;a href=&#34;https://www.ansible.com&#34;&gt;Ansible&lt;/a&gt;. Again, mostly because I worked with it before and because all I need to get started is a SSH key on the target system. The following steps will assume that you already have a running CentOS 8 system (likely a VM) running somewhere in your private network and that you have access to it. I won&amp;rsquo;t go into the basics of Ansible and I recommend you to search for tutorials / getting started guides on it before tagging along.&lt;/p&gt;
&lt;p&gt;At the end of this, we will have a private DNS resolver that is capable of resolving public names (with some filtering in place) and resolving one or more private domains we can adjust to our needs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; You should make sure that port 53 (DNS) of your system is not available on the public internet, securing access with firewall or security group rules.&lt;/p&gt;
&lt;h2 id=&#34;required-packages&#34;&gt;Required packages&lt;/h2&gt;
&lt;p&gt;Depending on the disk/CD image you used to install CentOS 8, not all required tools will be available right away. Besides the container runtime we want to use (&lt;code&gt;podman&lt;/code&gt;), let&amp;rsquo;s install two more packages: &lt;code&gt;udica&lt;/code&gt; and &lt;code&gt;bind-utils&lt;/code&gt;. &lt;code&gt;bind-utils&lt;/code&gt; is just useful as it contains the &lt;code&gt;dig&lt;/code&gt; command, something we can use to query our DNS resolver later on.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/containers/udica&#34;&gt;udica&lt;/a&gt; is more interesting: It hasn&amp;rsquo;t been mentioned yet, but I would like to integrate with existing security mechanisms on CentOS as much as possible. This means that &lt;strong&gt;we will keep SELinux in &lt;code&gt;enforcing&lt;/code&gt; mode&lt;/strong&gt;. &lt;code&gt;udica&lt;/code&gt; will enable us to generate SELinux modules that will allow running CoreDNS in a podman container while keeping SELinux up.&lt;/p&gt;
&lt;p&gt;All things considered, translated to a task in Ansible this would mean adding this to your &lt;code&gt;coredns&lt;/code&gt; role:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;install base packages&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;dnf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;l&#34;&gt;podman&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;l&#34;&gt;udica&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;l&#34;&gt;bind-utils&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;present&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Later on, I will not present tasks for trivial operations in Ansible - I strongly recommend to adapt my findings to your needs.&lt;/p&gt;
&lt;h2 id=&#34;setting-up-a-corefile&#34;&gt;Setting up a Corefile&lt;/h2&gt;
&lt;p&gt;Before we get too much into the details of deploying CoreDNS in podman, we should focus on defining our requirements for the DNS resolver and templating a &lt;code&gt;Corefile&lt;/code&gt; around that.&lt;/p&gt;
&lt;p&gt;We need to ensure that on the target system, there is a directory we can put our configuration into. My Ansible role creates &lt;code&gt;/etc/coredns&lt;/code&gt; for that purpose:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;add directory for CoreDNS configuration&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;file&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;path&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;/etc/coredns&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;directory&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;mode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;0755&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The bad news first: CoreDNS does not come with any kind of built in ad block solution. A lot of people are running services like Pi-hole on their private network and our solution won&amp;rsquo;t get as sophisticated as that. The good news is, if you want to block nepharious websites in your DNS resolver, you can still do that. Earlier we defined the requirement for our resolver to resolve all kinds of names, so let&amp;rsquo;s look into public names first.&lt;/p&gt;
&lt;h3 id=&#34;resolve-public-dns-with-a-blocklist&#34;&gt;Resolve public DNS, with a blocklist&lt;/h3&gt;
&lt;p&gt;To implement some kind of blocklist for bad DNS names, we can use the &lt;a href=&#34;https://coredns.io/plugins/hosts/&#34;&gt;hosts&lt;/a&gt; plugin of CoreDNS. It reads the content of a &lt;code&gt;/etc/hosts&lt;/code&gt;-style file and serves requests based on that. I have included a list from &lt;a href=&#34;https://github.com/StevenBlack/hosts&#34;&gt;StevenBlack/hosts&lt;/a&gt; as a blocklist that will resolve the hosts on that list to &lt;code&gt;0.0.0.0&lt;/code&gt;, thus failing resolution. Download one of the lists (depending on what you would like to block) into your Ansible role and copy them to your target system.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s set up the part in our &lt;code&gt;Corefile&lt;/code&gt; that will allow our resolver to resolve public names with the restrictions of our blocklist in place. To do so, we can ask CoreDNS to look into our blocklist and &lt;code&gt;fallthrough&lt;/code&gt; to the next lookup method if a name is not on our naughty list. We will rely on &lt;code&gt;/etc/resolv.conf&lt;/code&gt; for upstream DNS, but you can adjust the &lt;a href=&#34;https://coredns.io/plugins/forward/&#34;&gt;forward&lt;/a&gt; plugin configuration we are including to suit your network situation.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;l&#34;&gt;. {&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;hosts /etc/coredns/blocklist.hosts {&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;fallthrough&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;}&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;forward . /etc/resolv.conf&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;log&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;}&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This short configuration snippet instructs CoreDNS in the following ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It defines a server block for &lt;code&gt;.&lt;/code&gt;, which is the root zone, meaning that any requests should go through this configuration.&lt;/li&gt;
&lt;li&gt;It uses the &lt;code&gt;hosts&lt;/code&gt; plugin to try resolving a name from &lt;code&gt;/etc/coredns/blocklist.hosts&lt;/code&gt; (our blocklist). If a name is not on that list, it will go to the next method of resolving the name.&lt;/li&gt;
&lt;li&gt;It forwards any further requests to the DNS servers defined in &lt;code&gt;/etc/resolv.conf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It enables logging of all incoming requests via the &lt;code&gt;log&lt;/code&gt; plugin.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;resolve-private-dns&#34;&gt;Resolve private DNS&lt;/h3&gt;
&lt;p&gt;Now that we cover public DNS, let&amp;rsquo;s look into setting up one or more private DNS domains on this resolver. I am using the &lt;code&gt;hosts&lt;/code&gt; plugin again to resolve a templated list of DNS names; you can adjust that to your needs and situation.&lt;/p&gt;
&lt;p&gt;First, let&amp;rsquo;s generate another &lt;code&gt;/etc/hosts&lt;/code&gt;-style file for our private zone. The minimal viable solution for this would be this Jinja2 template:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-jinja&#34; data-lang=&#34;jinja&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;{%&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;entry&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;coredns_entries&lt;/span&gt; &lt;span class=&#34;cp&#34;&gt;%}&lt;/span&gt;&lt;span class=&#34;x&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;x&#34;&gt;&lt;/span&gt;&lt;span class=&#34;cp&#34;&gt;{{&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;entry.ip&lt;/span&gt; &lt;span class=&#34;cp&#34;&gt;}}&lt;/span&gt;&lt;span class=&#34;x&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;cp&#34;&gt;{{&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;entry.host&lt;/span&gt; &lt;span class=&#34;cp&#34;&gt;}}&lt;/span&gt;&lt;span class=&#34;x&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;x&#34;&gt;&lt;/span&gt;&lt;span class=&#34;cp&#34;&gt;{%&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;endfor&lt;/span&gt; &lt;span class=&#34;cp&#34;&gt;%}&lt;/span&gt;&lt;span class=&#34;x&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This requires passing the host variable &lt;code&gt;coredns_entries&lt;/code&gt; to our target host in Ansible - A simple &lt;code&gt;coredns_entries&lt;/code&gt; configuration could look like this (let&amp;rsquo;s throw in a variable for our private DNS zone for good measure):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;coredns_internal_domain&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;privatezone.internal&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;coredns_entries&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;ip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;10.0.0.1&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;host&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;gateway.privatezone.internal&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;ip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;10.0.0.2&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;host&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;gitlab.privatezone.internal&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can get smarter with this (e.g. using the &lt;code&gt;coredns_internal_domain&lt;/code&gt; variable to eliminate the requirement for adding a fqdn each time), but this is the bare minimum that will get us started. If it&amp;rsquo;s possible, consider pulling this kind of information out of your inventory. Ansible should template this to the target system, e.g. into &lt;code&gt;/etc/coredns/internal.hosts&lt;/code&gt;. Afterwards, adjust the &lt;code&gt;Corefile&lt;/code&gt; (that should become a template at this point) to load our internal hosts file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-jinja&#34; data-lang=&#34;jinja&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;{{&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;coredns_internal_domain&lt;/span&gt; &lt;span class=&#34;cp&#34;&gt;}}&lt;/span&gt;&lt;span class=&#34;x&#34;&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;x&#34;&gt;    hosts /etc/coredns/internal.hosts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;x&#34;&gt;    log
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;x&#34;&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;x&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;x&#34;&gt;. {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;x&#34;&gt;    hosts /etc/coredns/blocklist.hosts {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;x&#34;&gt;        fallthrough
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;x&#34;&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;x&#34;&gt;    forward . /etc/resolv.conf
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;x&#34;&gt;    log
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;x&#34;&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There we go! It&amp;rsquo;s important to load your private zones before the big catch-all &lt;code&gt;.&lt;/code&gt; block, so CoreDNS won&amp;rsquo;t try to send your private hostnames to public DNS resolvers. Now we have a minimal &lt;code&gt;Corefile&lt;/code&gt; on our target system. Let&amp;rsquo;s look into running CoreDNS as a podman container next.&lt;/p&gt;
&lt;h2 id=&#34;running-coredns-in-podman&#34;&gt;Running CoreDNS in Podman&lt;/h2&gt;
&lt;p&gt;The next thing we want to do is figuring out the correct &lt;code&gt;podman&lt;/code&gt; command we want to run to start our CoreDNS container. While there is an equivalent to &lt;code&gt;docker-compose&lt;/code&gt; available, let&amp;rsquo;s stay with the (mostly &lt;code&gt;docker&lt;/code&gt;-compatible) &lt;code&gt;podman&lt;/code&gt; cli. We will later wrap this call in systemd to leverage it as supervisor for our container.&lt;/p&gt;
&lt;p&gt;The full &lt;code&gt;podman run&lt;/code&gt; call that I came up with is this one:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;/usr/bin/podman run --name coredns --read-only -p 10.0.0.1:53:53/tcp -p 10.0.0.1:53:53/udp -v /etc/coredns:/etc/coredns:ro --cap-drop ALL --cap-add NET_BIND_SERVICE coredns/coredns:1.7.0 -conf /etc/coredns/Corefile
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Let&amp;rsquo;s look at the parameters passed here to understand what is going on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--name coredns&lt;/code&gt; will set the container name to &amp;ldquo;coredns&amp;rdquo;. This means that running another instance of this command will conflict with any running or stopped one.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--read-only&lt;/code&gt; sets the container&amp;rsquo;s root filesystem to read-only mode. The root filesystem of a container is transient, so applications should not store any important data there anyway. CoreDNS doesn&amp;rsquo;t need write access to its root filesystem, so let&amp;rsquo;s disable this to improve isolation.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p 10.0.0.1:53:53/tcp&lt;/code&gt; and &lt;code&gt;-p 10.0.0.1:53:53/udp&lt;/code&gt; bind to port 53 on both TCP and UDP for the private IP the system; replace this with your own system&amp;rsquo;s private IP (or rather, use Jinja templating for it; we will follow up on that). Binding to the IP might be necessary because &lt;code&gt;systemd-resolved&lt;/code&gt; is possibly taking the port on &lt;code&gt;localhost&lt;/code&gt;, making it impossible to bind to.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v /etc/coredns:/etc/coredns:ro&lt;/code&gt; mounts our configuration folder into the container. Again, this is read-only, to ensure integrity of our configuration files. CoreDNS has no need to write back into this directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--cap-drop ALL&lt;/code&gt; and &lt;code&gt;--cap-add NET_BIND_SERVICE&lt;/code&gt; will drop as much &lt;a href=&#34;https://man7.org/linux/man-pages/man7/capabilities.7.html&#34;&gt;Linux capabilities&lt;/a&gt; as possible while maintaining &lt;code&gt;NET_BIND_SERVICE&lt;/code&gt;, which is required to bind to the port. Again, this improves isolation and &lt;a href=&#34;https://www.redhat.com/en/blog/secure-your-containers-one-weird-trick&#34;&gt;is considered good practice to reduce attack surfaces&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All in all, this tries to limit the application running in the container in what it is capable of doing.&lt;/p&gt;
&lt;p&gt;Now, if you try to run this command, it will hopefully fail - CoreDNS will be unable to bind to port 53 (if it does not fail or complain in the logs, you probably disabled SELinux previously - &lt;a href=&#34;https://stopdisablingselinux.com/&#34;&gt;shame on you!&lt;/a&gt;). SELinux does not allow it yet. Let&amp;rsquo;s fix that.&lt;/p&gt;
&lt;h3 id=&#34;generating-a-selinux-policy&#34;&gt;Generating a SELinux policy&lt;/h3&gt;
&lt;p&gt;Remember when we installed &lt;code&gt;udica&lt;/code&gt; in the beginning? Now its time to shine has come! &lt;code&gt;udica&lt;/code&gt; is a small but awesome tool that allows generating SELinux policies from container definitions. It takes away a lot of pain with containers and active SELinux. With that being said, I urge you to read up on SELinux and how it works, giving you the opportunity to further slim down policies if possible.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s pull the definition of our &amp;ldquo;coredns&amp;rdquo; container, put it into a JSON file and pass it to &lt;code&gt;udica&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ podman inspect coredns &amp;gt; container.json
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ udica -j container.json coredns
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Policy coredns created!
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Please load these modules using: 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# semodule -i coredns.cil /usr/share/udica/templates/{base_container.cil,net_container.cil}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Restart the container with: &lt;span class=&#34;s2&#34;&gt;&amp;#34;--security-opt label=type:coredns.process&amp;#34;&lt;/span&gt; parameter
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This generates a &lt;code&gt;coredns.cil&lt;/code&gt; file in the current directory. For me, it looked like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;l&#34;&gt;(block coredns&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;(blockinherit container)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;(blockinherit restricted_net_container)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;(allow process process ( capability ( net_bind_service )))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;(allow process dns_port_t ( tcp_socket (  name_bind )))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;(allow process dns_port_t ( udp_socket (  name_bind )))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;(allow process etc_t ( dir ( getattr search open read lock ioctl )))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;(allow process etc_t ( file ( getattr read ioctl lock open  )))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;(allow process etc_t ( sock_file ( getattr read open  )))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;l&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;s relatively small and quite declarative, so the main takeaways from this policy should be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it will allow using the &lt;code&gt;net_bind_service&lt;/code&gt; capability.&lt;/li&gt;
&lt;li&gt;it will allow the process to bind to ports labeled &lt;code&gt;dns_port_t&lt;/code&gt; for UDP and TCP (which is only port 53).&lt;/li&gt;
&lt;li&gt;it will allow the process to read from directories labeled &lt;code&gt;etc_t&lt;/code&gt; (which is &lt;code&gt;/etc&lt;/code&gt;, where our CoreDNS configuration directory is).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You could further lock down this policy with your own SELinux labels on &lt;code&gt;/etc/coredns&lt;/code&gt;, but for now, this should suffice. It will get us past SELinux as a gatekeeper and improve our security posture because we didn&amp;rsquo;t disable SELinux (yay). To enable this policy, run the command in the &lt;code&gt;udica&lt;/code&gt; output.&lt;/p&gt;
&lt;h3 id=&#34;interlude-selinux-policies-and-ansible&#34;&gt;Interlude: SELinux policies and Ansible&lt;/h3&gt;
&lt;p&gt;As all configuration steps, this should be included in our Ansible role to make the installation reproducible on fresh systems. Unfortunately, Ansible doesn&amp;rsquo;t seem to offer a module that will allow applying this, which means we need to fall back to using the &lt;code&gt;shell&lt;/code&gt; module. This is how my role handles applying this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# tasks/main.yml&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;l&#34;&gt;...]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;create /etc/udica for custom SELinux policies&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;file&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;path&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;/etc/udica&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;directory&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;mode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;0755&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;create coredns SELinux policy file&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;copy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;src&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;files/coredns.cil&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;dest&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;/etc/udica/coredns.cil&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;notify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;l&#34;&gt;load coredns SELinux module&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;l&#34;&gt;...]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# handlers/main.yml&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;load coredns SELinux module&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;shell&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;semodule -i /etc/udica/coredns.cil /usr/share/udica/templates/{base_container.cil,net_container.cil}&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This creates a handler running &lt;code&gt;semodule&lt;/code&gt; which is being called upon changes to the SELinux policy file.&lt;/p&gt;
&lt;h3 id=&#34;systemd-service&#34;&gt;systemd service&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;re almost done! At this point, our CoreDNS container should run successfully and you should be able to resolve hosts with &lt;code&gt;dig @10.0.0.1 google.com&lt;/code&gt; (replace &lt;code&gt;10.0.0.1&lt;/code&gt; with your own private IP) as long as you&amp;rsquo;re running the &lt;code&gt;podman&lt;/code&gt; command from earlier.&lt;/p&gt;
&lt;p&gt;But as mentioned, &lt;code&gt;podman&lt;/code&gt; doesn&amp;rsquo;t come with a supervising daemon - Which means that no one will restart the container once it&amp;rsquo;s dead (for whatever reason); Nor will it be restarted upon reboot.&lt;/p&gt;
&lt;p&gt;To fix this, let&amp;rsquo;s use something on our system that is already working as a supervisor to services: systemd! Fortunately, integration with systemd works quite well under &lt;code&gt;podman&lt;/code&gt;, as long as you follow &lt;a href=&#34;https://www.redhat.com/sysadmin/podman-shareable-systemd-services&#34;&gt;a template recommended by Red Hat&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is the Jinja template that I use to generate &lt;code&gt;/etc/systemd/system/coredns.service&lt;/code&gt;, which is based on the service unit template linked above. The main difference to a standard systemd service unit is that &lt;code&gt;podman&lt;/code&gt; will write to PID files that systemd will read to be informed about the container status.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-systemd&#34; data-lang=&#34;systemd&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;Description&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;CoreDNS private DNS in a container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;Restart&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;on-failure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;ExecStartPre&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;/usr/bin/rm -f /%t/%n-pid /%t/%n-cid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;ExecStartPre&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;-/usr/bin/podman rm -f coredns&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;ExecStart&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;/usr/bin/podman run --name coredns --read-only --security-opt label=type:coredns.process -p {{ ansible_eth1.ipv4.address }}:53:53/tcp -p {{ ansible_eth1.ipv4.address }}:53:53/udp -v /etc/coredns:/etc/coredns:ro --cap-drop ALL --cap-add NET_BIND_SERVICE --conmon-pidfile /%t/%n-pid --cidfile /%t/%n-cid -d coredns/coredns:1.7.0 -conf /etc/coredns/Corefile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;ExecStop&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;/usr/bin/podman stop coredns&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;KillMode&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;none&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;Type&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;forking&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;PIDFile&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;/%t/%n-pid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;[Install]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;WantedBy&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;multi-user.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Beyond what the template requires, note the additions in comparison to the previous iteration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--security-opt label=type:coredns.process&lt;/code&gt; will allow the container to use our previously created SELinux policy.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p {{ ansible_eth1.ipv4.address }}:53:53/tcp&lt;/code&gt; and &lt;code&gt;-p {{ ansible_eth1.ipv4.address }}:53:53/udp&lt;/code&gt; will template in the private IP of my system, which is associated with &lt;code&gt;eth1&lt;/code&gt;. Adjust to your situation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This will deliver a fully functional systemd unit called &lt;code&gt;coredns.service&lt;/code&gt; that will utilise &lt;code&gt;podman&lt;/code&gt; and a targeted SELinux policy to run CoreDNS on your local network. Do not forget to write your Ansible role in a way that changes to the SELinux policy or your configuration files will trigger a restart.&lt;/p&gt;
&lt;h2 id=&#34;wrapping-up&#34;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;That&amp;rsquo;s it - We now have CoreDNS running, safely wrapped into &lt;code&gt;podman&lt;/code&gt;, SELinux and systemd. Using Ansible is a nice bonus, allowing us to deploy our private DNS resolver to additional systems with little overhead. Not all steps have been presented as Ansible snippets, but I hope it will prove useful to those writing better roles than I do.&lt;/p&gt;
&lt;p&gt;If everything worked well, we will now see a container running in &lt;code&gt;podman&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ podman ps
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;CONTAINER ID  IMAGE                            COMMAND               CREATED       STATUS           PORTS                                         NAMES
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;16f66c7140ed  docker.io/coredns/coredns:1.7.0  -conf /etc/coredn...  &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; hours ago  Up &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; hours ago  10.0.0.1:53-&amp;gt;53/tcp, 10.0.0.1:53-&amp;gt;53/udp  coredns
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To access the logs (which includes all requests the server received and answered), we can run &lt;code&gt;podman logs coredns&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This setup obviously has further room for improvement. Next steps to explore could be (but are not limited to):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pull the hostnames of our private dns zone out of the Ansible inventory during templating.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;cache&lt;/code&gt; plugin in CoreDNS to improve performance and reduce the need to reach out to upstream DNS resolvers.&lt;/li&gt;
&lt;li&gt;Investigate other CoreDNS plugins for host discovery, exploring the possibilities of &lt;code&gt;Corefile&lt;/code&gt; configuration.&lt;/li&gt;
&lt;li&gt;Expose metrics via the &lt;code&gt;prometheus&lt;/code&gt; plugin. This would require running a Prometheus server somewhere on your internal network, but improving visibility on your services is worthwhile.&lt;/li&gt;
&lt;li&gt;Look into implementing &lt;a href=&#34;https://developers.redhat.com/blog/2019/04/18/monitoring-container-vitality-and-availability-with-podman/&#34;&gt;a health check via podman&lt;/a&gt; to catch any service malfunction that might not result in the container crashing.&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    
  </channel>
</rss>

